ユーザが任意入力されたデータを要約してランキング形式で表示させるサイトをOpenAIを組み合わせて作成してみました。ランキングはscikit-learnのTfidfVectorizerとKMeansを使用して、DBに保存されたテキストをクラスタリングし、類似テキストをグループ化することで件数をカウント。ラインキング表示させる形になっています。
前提
OS:CentOS7.9
Docker: 23.0.1, build a5ee5b1
docker-compose: 1.28.2, build 67630359
Python:3.9.2
node: v19.9.0
npm: 9.6.3
APIキー:OpenAIにて事前作成しておく
1. コンテナ作成
任意のディレクトリ上で、Dockerfile,docker-compose.ymlを作成してください。
FROM node:19-slim
RUN apt-get update && apt-get install -y vim curl git python3 python3-dev python3-pip procps sqlite3
RUN npm install -g create-react-app
RUN npm install -g npm@9.6.3
RUN pip3 install uvicorn fastapi openai==0.28 scikit-learn
version: '3'
services:
api:
image: rankings-api:0.1
restart: always
build: .
container_name: 'rankings-api'
tty: true
volumes:
- ./work:/opt
ports:
- '4001:4001'
- '4002:4002'
続いて、workディレクトリ作成後のcomposeを使ってコンテナを起動します。
mkdir work
docker-compose up -d
2. FastAPI準備
コンテナ起動後に、FastAPIを利用したAPIを作成します。以下の順にコマンドを実行します。
mkdir work/fastapi
cd work/fastapi
エディタ(vim)などでmain.pyファイルを作成します。openai.api_keyは自分で取得したAPIキーを入力してください。React側からは文字入力を受け付けるエンドポイント(submit)と要約されたデータを返却するエンドポイント(rankings)を作成しています。
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import sqlite3
import openai
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans
import numpy as np
openai.api_key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXX' #自身のAPIキー
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
)
conn = sqlite3.connect('ranking.db', check_same_thread=False)
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS entries (id INTEGER PRIMARY KEY, text TEXT)''')
conn.commit()
class InputData(BaseModel):
text: str
@app.post("/submit/")
async def submit(data: InputData):
c.execute("INSERT INTO entries (text) VALUES (?)", (data.text,))
conn.commit()
return {"message": "Successfully"}
def group_similar_texts(texts, num_clusters):
try:
vectorizer = TfidfVectorizer().fit_transform(texts)
vectors = vectorizer.toarray()
kmeans = KMeans(n_clusters=num_clusters, random_state=0).fit(vectors)
clusters = {}
for idx, label in enumerate(kmeans.labels_):
if label not in clusters:
clusters[label] = []
clusters[label].append(texts[idx])
return list(clusters.values())
except Exception as e:
print(f"Error in group_similar_texts: {e}")
return []
@app.get("/rankings/")
async def get_rankings():
c.execute("SELECT text FROM entries")
rows = c.fetchall()
texts = [row[0] for row in rows]
if not texts:
return {"rankings": []}
grouped_texts = group_similar_texts(texts, num_clusters=2)
summaries = []
for group in grouped_texts:
combined_text = " ".join(group)
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": f"次の文章を要約して下さい: {combined_text}"}
],
max_tokens=100,
temperature=0.0
)
summary = response.choices[0].message['content'].strip()
summaries.append((combined_text, summary, len(group)))
sorted_summaries = sorted(summaries, key=lambda x: x[2], reverse=True)
rankings = [{"combined_text": s[0], "summary": s[1], "count": s[2]} for s in sorted_summaries]
return {"rankings": rankings}
類似事象としてグループ化させるために、sklearnのTfidfVectorizer+KMeansを使用しています。TfidfVectorizerとKMeansは以下のサイトを参考にさせていただきました。
TfidfVectorizer
KMeans
3. FastAPI起動
以下コマンドを実行してuvicornからFastAPIを起動します。
docker exec -it rankings-api /bin/bash
cd /opt/fastapi
uvicorn main:app --host 0.0.0.0 --port 4002
4. Reactコード作成
次に、React側の準備に取り掛かります。別ターミナルを立ち上げて、以下のコマンドを順に実行しましょう。
docker exec -it rankings-api /bin/bash
cd /opt/
npx create-react-app app
cd /opt/app
npm install axios @mui/material @emotion/react @emotion/styled @mui/icons-material
src/App.jsを以下のように変更します。
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import {
Container,
TextField,
Button,
List,
Typography,
Paper,
Box,
Card,
CardContent,
CardActions,
IconButton,
CssBaseline
} from '@mui/material';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import SendIcon from '@mui/icons-material/Send';
import Brightness4Icon from '@mui/icons-material/Brightness4';
import Brightness7Icon from '@mui/icons-material/Brightness7';
function App() {
const [input, setInput] = useState('');
const [rankings, setRankings] = useState([]);
const [themeMode, setThemeMode] = useState(false);
const theme = createTheme({
palette: {
mode: themeMode ? 'light' : 'dark',
},
});
const handleSubmit = async () => {
await axios.post('http://x.x.x.x:4002/submit/', { text: input }); // IPはDockerホストのIP
setInput('');
};
useEffect(() => {
const fetchRankings = async () => {
const response = await axios.get('http://x.x.x.x:4002/rankings/'); // IPはDockerホストのIP
setRankings(response.data.rankings);
};
// 1分ごとにリフレッシュ
const intervalId = setInterval(fetchRankings, 60000);
// インターバルのクリーンアップ
return () => clearInterval(intervalId);
}, []);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Container maxWidth="md">
<Paper elevation={3} style={{ padding: '2rem', marginTop: '2rem' }}>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h4" component="h1" gutterBottom>
トラブル報告
</Typography>
<IconButton onClick={() => setThemeMode(!themeMode)}>
{themeMode ? <Brightness7Icon /> : <Brightness4Icon />}
</IconButton>
</Box>
<Box component="form" noValidate autoComplete="off" onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<TextField
label="お困りごとを入力してください。"
variant="outlined"
fullWidth
value={input}
onChange={(e) => setInput(e.target.value)}
style={{ marginBottom: '1rem' }}
/>
<Button
variant="contained"
color="primary"
fullWidth
onClick={handleSubmit}
endIcon={<SendIcon />}
>
送信
</Button>
</Box>
</Paper>
<Typography variant="h5" component="h2" style={{ marginTop: '2rem' }}>
トラブルダイジェスト(ランキング順)
</Typography>
<List>
{rankings.map((ranking, index) => (
<Card key={index} style={{ marginBottom: '1rem' }}>
<CardContent>
<Typography color="textSecondary" gutterBottom>
トラブル事象(要約):
</Typography>
<Typography variant="body2" component="p">
{ranking.summary}
</Typography>
</CardContent>
<CardActions>
<Typography variant="caption" color="textSecondary" style={{ marginLeft: 'auto' }}>
件数: {ranking.count}
</Typography>
</CardActions>
</Card>
))}
</List>
</Container>
</ThemeProvider>
);
}
export default App;
これで準備完了です。
5. Reactアプリ起動
Sourceも変更できたので、これでアプリを起動できます。起動前に.envファイルを作成して起動するポートを4001番に変更します。
PORT=4001
では下記コマンドで起動しましょう。
npm start
6. アプリケーション確認
ブラウザでアクセスします。http://x.x.x.x:4001 でアクセスしましょう。
Inputboxにいくつかデータを登録すると以下のように表示されます!
分類を細かくしたい場合、num_clusters=2の数値を大きくしてください。(num_clusters=5の例)
分類が思うようにいかない場合は、Janomeなどの形態素解析ライブラリを利用した上でクラスタリング化するとよいかもしれません。
Janome
また、OpenAIへのリクエストは60秒間隔で実行されるようになっています。間隔を調整する場合、App.jsのintervalIdを調整してください。
App.js
const intervalId = setInterval(fetchRankings, 60000);
7. その他
機械学習ライブラリ+OpenAIなどを組み合わせることで、様々な効果を持つサイトができると思いました。記事が誰かの役に立てば幸いです。