Site Search

[Trying out AI persona marketing with NVIDIA DGX Spark]
Episode 1: Overview of the Demo
Episode 2: Selection of the Subjects to be Investigated
Episode 3: Question Generation
Episode 4: Generating and Collecting Responses
Episode 5: Visualizing the Survey Results
Episode 6 Summary

Contents of this story

In this episode, we will explain the core of this demonstration: how to answer questionnaires using LLM. The input data for LLM is created by selecting one fictional character at a time from the research subjects extracted in Episode 2, and adding the content of the questionnaire to them. (A questionnaire will be generated for each research subject.) These are input into LLM, and the output from LLM is the questionnaire responses (the selected options and comments for each question).

Setting up the LLM API endpoint and configuring the client

We will start the GPT-OSS-120B API endpoint in the same way we did when creating the question in Episode 3.

import json
import pandas as pd
from openai import AsyncOpenAI, APIError
from survey import Survey, SurveyResponse, SurveyResponseWithInfo, SurveyResponseList
import time
from tqdm.asyncio import tqdm_asyncio
import asyncio
from IPython.display import display, Markdown
# LLM APIエンドポイントの設定 MODEL = "openai/gpt-oss-120b" BASE_URL = "http://localhost:8000/v1" CONCURRENCY_LIMIT = 128
# OpenAIクライアントの初期化 client = AsyncOpenAI( base_url=BASE_URL, api_key="None" )

Creating a questionnaire

We will now load the survey questions and answer choices created in Episode 3.

# 調査票データの取得 with open("survey.json", encoding="utf-8") as f: json_str = json.dumps(json.load(f), ensure_ascii=False) survey_data = Survey.model_validate_json(json_str)

The retrieved data will be formatted into Markdown format.

# 調査票の作成 questionnaire = "## 質問¥n¥n" num = 1 for q in survey_data.questions: questionnaire += f"### Q{num}. {q.question_text}¥n" for idx, option in enumerate(q.choices): questionnaire += f" {idx+1}. {option.text}¥n" questionnaire += "¥n" num += 1 questionnaire += "各質問に対して、選択肢の番号で回答し、自由記述欄に選択理由を追加してください。¥n" print(questionnaire)
## 質問 ### Q1. 朝の目覚めに、あなたが思い浮かべる“さわやかさ”はどのシーンですか? 1. 海辺の朝日が水平線から顔を出す瞬間 2. 高原の風が葉を揺らす音と共に吹く瞬間 3. 都会のカフェで、窓の外に映るネオンが光る瞬間 4. 秋の森の中で、落ち葉がカサカサと音を立てる瞬間 ### Q2. コンビニで新しいスナックを選ぶとき、どのパッケージが最初に手に取られそうですか? 1. 淡いパステルカラーでシンプルなロゴが配置されたデザイン 2. 鮮やかなネオンカラーとダイナミックなパターンが走るデザイン 3. レトロなイラストと温かみのあるトーンでまとめられたデザイン 4. 黒を基調にメタリックなアクセントが入ったモダンなデザイン ### Q3. 友達と週末に新しいお菓子をシェアするとしたら、どんなシチュエーションが一番楽しいですか? 1. 自然の中でハイキングやピクニックをしながら 2. 屋外フェスやライブで音楽に合わせて 3. カフェでゆっくりおしゃべりしながら 4. 自宅でゲームや映画鑑賞をしながら ### Q4. テレビやSNSで有名人が商品を紹介しているとき、あなたが「信頼できる」と感じるのはどんな雰囲気ですか? 1. 自然体で笑いを交えたユーモラスな雰囲気 2. 自分の経験や感情を率直に語る誠実さ 3. 専門的な知識やスキルを活かした説得力 4. 最新トレンドを先取りするクールさ ### Q5. 新しいスナックを食べるとき、どのような音楽が流れていると最もリラックスできますか? 1. 軽快なポップス 2. リズミカルなヒップホップ 3. 落ち着いたインディー・アコースティック 4. エレガントなジャズやクラシック 各質問に対して、選択肢の番号で回答し、自由記述欄に選択理由を追加してください。

We will create a function to retrieve the data of the survey participants extracted in Episode 1 and format it into Markdown format.

# ペルソナデータの取得 persona_df = pd.read_json("personas.jsonl", orient="records", lines=True)
# Pandasデータフレームのシリーズオブジェクトを受け取り、UUID列を除外してMarkdown形式のレポートを生成する関数 def generate_persona_report(ser): report = "## ペルソナ¥n¥n" for index, value in ser.items(): if index == "uuid": continue report += f"### {index}¥n" report += f"{value}¥n¥n" return report

The questions, formatted in Markdown, and the survey participant data (6 personas and 16 attributes), also formatted in Markdown, are combined to create a questionnaire for one participant.

# 調査票のテスト表示 row = persona_df.iloc[0] persona_report = generate_persona_report(row) user_prompt = "# グミ菓子新製品に関する消費者調査¥n¥n" user_prompt += (persona_report + questionnaire) # display(Markdown(user_prompt)) print(user_prompt)
# グミ菓子新製品に関する消費者調査 ## ペルソナ ### professional_persona 伊藤 時生は、実務的なUI/UXデザインを中小企業のデザイン部門でリーダーシップに変換し、Figmaとデザインシステムでチームのスキル向上と柔軟なフィードバックループを構築することに注力している。彼らは、実用性を重視した提案と、協調性を活かしたクライアント調整で、成果志向と創造性のバランスを保っている。 ### sports_persona 伊藤 時生は、都市部のカフェ巡りと自転車での写真撮影を組み合わせ、低インパクトながらも継続的なエクササイズルーティンを保っている。彼らは、外向性と協調性を活かし、仲間と同時に走ることを好みつつ、柔軟なスケジュールで運動負荷を調整している。 ### arts_persona 伊藤 時生は、手作りの陶芸と木工で触覚的な作品を生み出し、実用的なデザインへの視覚的洞察と結びつけている。彼らは、開放性の中に伝統的な素材尊重と、協力的なワークショップでの対話を通じ、感情の起伏と創作のバランスを探求している。 ### travel_persona 伊藤 時生は、街中のカフェでメニューの視覚要素を観察し、週末の自転車で新たな街区を撮影しながら、低予算でも都市探索を実践している。彼らは、外向性と柔軟性を組み合わせ、計画的なルートと即興的な立ち寄りを交互に取り入れ、都市の風景と人間関係の両面に敏感に反応している。 ### culinary_persona 伊藤 時生は、カフェのメニュー構成と自作のシンプルレシピで、視覚と味覚の調和を追求し、低コストでも創造的な料理体験を提供している。彼らは、神経症傾向の敏感さから、食材の鮮度と調理手順を細部まで管理し、実用的な料理とデザインの交差点で自己表現を行っている。 ### persona 伊藤 時生は、実務的デザインリーダーとして、低予算の手作業と都市自転車撮影を通じ、柔軟な計画と感情管理でチームと自己の成長を同時に追求している。 ### cultural_background 関東の都市部で育ち、家族や学校での勤勉さと忍耐が重視された環境を背景に、実務的なデザインへの取り組みを大切にしている。 ### skills_and_expertise ユーザー体験を重視したUI/UXデザイン、Adobe Creative Cloud全般、Figmaでのプロトタイピング、HTML/CSSによる簡易実装、デザインシステムの構築、クライアントとの要件調整とフィードバックループの管理。 ### skills_and_expertise_list ["UI/UXデザイン", "Adobe Creative Cloud", "Figmaプロトタイピング", "HTML/CSS実装", "デザインシステム構築", "要件調整・フィードバック管理"] ### hobbies_and_interests 街中のカフェ巡りでインテリアやメニューの視覚要素を観察すること、週末の自転車で都心の風景を撮影しながら構図を学ぶこと、低予算で手作りの陶芸や木工に挑戦し、実感のある成果を楽しむこと。 ### hobbies_and_interests_list ["カフェ巡り・インテリア観察", "自転車撮影・風景構図研究", "手作り陶芸・木工"] ### career_goals_and_ambitions 中小企業のデザイン部門でデザインリーダーシップを発揮し、実務的かつユーザー志向のプロダクトを増やすと同時に、チームのスキル向上と働きやすい環境作りに貢献したい。 ### sex 男 ### age 29 ### marital_status 未婚 ### education_level 大学卒 文系 ### occupation デザイン 中小 ### region 関東地方 ### area 東日本 ### prefecture 東京都 ### country 日本 ## 質問 ### Q1. 朝の目覚めに、あなたが思い浮かべる“さわやかさ”はどのシーンですか? 1. 海辺の朝日が水平線から顔を出す瞬間 2. 高原の風が葉を揺らす音と共に吹く瞬間 3. 都会のカフェで、窓の外に映るネオンが光る瞬間 4. 秋の森の中で、落ち葉がカサカサと音を立てる瞬間 ### Q2. コンビニで新しいスナックを選ぶとき、どのパッケージが最初に手に取られそうですか? 1. 淡いパステルカラーでシンプルなロゴが配置されたデザイン 2. 鮮やかなネオンカラーとダイナミックなパターンが走るデザイン 3. レトロなイラストと温かみのあるトーンでまとめられたデザイン 4. 黒を基調にメタリックなアクセントが入ったモダンなデザイン ### Q3. 友達と週末に新しいお菓子をシェアするとしたら、どんなシチュエーションが一番楽しいですか? 1. 自然の中でハイキングやピクニックをしながら 2. 屋外フェスやライブで音楽に合わせて 3. カフェでゆっくりおしゃべりしながら 4. 自宅でゲームや映画鑑賞をしながら ### Q4. テレビやSNSで有名人が商品を紹介しているとき、あなたが「信頼できる」と感じるのはどんな雰囲気ですか? 1. 自然体で笑いを交えたユーモラスな雰囲気 2. 自分の経験や感情を率直に語る誠実さ 3. 専門的な知識やスキルを活かした説得力 4. 最新トレンドを先取りするクールさ ### Q5. 新しいスナックを食べるとき、どのような音楽が流れていると最もリラックスできますか? 1. 軽快なポップス 2. リズミカルなヒップホップ 3. 落ち着いたインディー・アコースティック 4. エレガントなジャズやクラシック 各質問に対して、選択肢の番号で回答し、自由記述欄に選択理由を追加してください。

Request a response from LLM

Prepare the system prompt as follows. The user prompt is the questionnaire created above.

system_prompt = """ あなたは一般消費者向け商品のマーケティング専門家です。 主に一般消費者からの意見をヒアリングし、分析する業務を行っています。 与えられたペルソナのつもりになって質問票に回答してください。 """

Asynchronous requests to LLM

By the way, when discussing the performance of an LLM inference server, latency and throughput are important metrics. Latency is the time from when a request is input until the inference result is output, while throughput is the number of tokens that can be processed per unit of time.

In chat applications, low latency is especially important, as high latency reduces usability. In applications like this demo, which process large amounts of data in batches, throughput is far more important than latency. One way to improve throughput, even at the expense of some latency, is to increase the parallelism of requests. This involves continuously submitting subsequent requests to the inference server up to the configured limit before the inference results of the previous request are returned, so that multiple requests are processed in batches and jobs are submitted without the GPU becoming idle.

However, even with hardware of the same class, the degree of request parallelism that can be handled varies depending on the inference server software. In this respect, vLLM, which was used in this demo, is excellent.

The following function is defined to obtain the survey results for a specified single survey participant. To handle request parallelisms exceeding 1, Python's asynchronous I/O functionality is used.

async def process_row(semaphore, row, system_prompt, retries=3): async with semaphore: persona_report = generate_persona_report(row) user_prompt = "# グミ菓子新製品に関する消費者調査¥n¥n" user_prompt += (persona_report + questionnaire) response = None for attempt in range(retries): try: response = await client.responses.parse( model=MODEL, input=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], text_format=SurveyResponse, temperature=0.2, top_p=0.7, ) responseWithInfo = SurveyResponseWithInfo( **response.output_parsed.model_dump(), persona=persona_report, input_tokens=response.usage.input_tokens, output_tokens=response.usage.output_tokens ) except APIError as e: print(f"APIError on attempt {attempt+1} for row {row['uuid']}: {e}") await asyncio.sleep(2 ** attempt) # Exponential backoff else: break return responseWithInfo

Also, just like when we created the questions in Episode 3, we will use the Structured Output feature to receive the responses as objects of the SurveyResponse class. The definitions of the SurveyResponse class and its related classes are as follows:

class ResponseItem(BaseModel): """調査票の1問に対する消費者の回答""" option: int # 質問に対する選択肢の番号 comments: str # 自由記述欄 class SurveyResponse(BaseModel): """調査票に対する消費者の回答全体""" responses: list[ResponseItem] class SurveyResponseWithInfo(SurveyResponse): """調査票に対する消費者の回答全体とペルソナ情報""" persona: str # 消費者のペルソナ情報 input_tokens: int # LLMへの入力トークン数 output_tokens: int # LLMからの出力トークン数 class SurveyResponseList(BaseModel): """調査票回答のリスト""" survey_responses: list[SurveyResponseWithInfo] def add_response(self, response: SurveyResponseWithInfo): self.survey_responses.append(response)

As shown below, a semaphore is used to set an upper limit on request parallelism. Experiments suggest that when running this demo on DGX Spark, it is best to set the parallelism (CONCURRENCY_LIMIT) to a value of around 128.

# 調査の実施 semaphore = asyncio.Semaphore(CONCURRENCY_LIMIT) responses = await tqdm_asyncio.gather(*[process_row(semaphore, row, system_prompt) for _, row in persona_df.iterrows()]) responses = [r for r in responses if r is not None] # Noneの結果を除外 print(f"Successfully processed {len(responses)} out of {len(persona_df)} rows.")

The following effects were observed from increasing request parallelism. Please note that the results may vary with each execution, so consider this information for reference only.

Request parallelism 1000 request processing time Total number of input tokens Total number of output tokens * 1
128 35 minutes 12 seconds 1915317 717160
1 6 hours 10 minutes 42 seconds 1915317 672107

[1] Even if the same prompt (the contents of the questionnaire in this demo) is entered, the number of output tokens will vary slightly each time it is run.

Save results

The results will be saved to a file as JSON data, as shown below.

# 調査結果の保存 survey_responses = SurveyResponseList(survey_responses=responses) survey_res_file_path = f"response{time.strftime('%Y%m%d%H%M%S')}.json" with open(survey_res_file_path, "w", encoding="utf-8") as f: json_data =json.loads(survey_responses.model_dump_json(ensure_ascii=False)) json.dump(json_data, f, indent=4, ensure_ascii=False) print(f"Survey responses saved to {survey_res_file_path}")

The total number of input tokens and total number of output tokens processed during the above process can be referenced as follows. If the questionnaire data created above is the same, the total number of input tokens will be the same each time, but the number of output tokens will differ slightly each time.

total_input_tokens = sum([r.input_tokens for r in responses])
total_output_tokens = sum([r.output_tokens for r in responses])
print(f"Total input tokens: {total_input_tokens}")
print(f"Total output tokens: {total_output_tokens}")
Total input tokens: 1915317
Total output tokens: 717160

Summary of this episode

This article introduced how to provide persona data to LLM and have it answer questionnaires. It also covered techniques for maximizing throughput by increasing the request parallelism of the inference server. Similar to generating the questionnaire with LLM, using LLM's Structured Output feature makes it easy to save responses as structured data, simplifying subsequent data aggregation.

We will explain the aggregation and visualization processes in the next episode.

Contact Us