技育CAMPキャラバンハッカソン＠東京参加レポート：マッチングエンジンの実装

# はじめに

私は先日、今年度最後の技育ハッカソン(1/25~2/3) に参加しました。この記事では、チーム内で私の担当したマッチングエンジンに焦点を絞って、使用技術と成果について共有したいと思います。また全体的なプロダクトについては、他のメンバーが紹介しているので、そちらをご覧下さい。

# 技育CAMPキャラバンとは

技育CAMPキャラバンは、サポーターズが運営する技育プロジェクトの一つで、1週間の事前開発期間を経て、現地でプロジェクトの発表を行うオフラインハッカソンです。2023年度は全国6都市で開催されました。また技育プロジェクトではオンライン勉強会や就職活動支援も行われているため、エンジニアを目指す若者にとって、非常に有益なイベントです。


# 結果
私たちのチームでは、フロントエンド2人、バックエンド２名の計4名で実装に挑み、結果としてGMOインターネットグループ賞を受賞することができました。開発の中では、バックエンドのAPI化や会場でのSSH接続トラブルが発生しましたが、なんとかプレゼンまで終えることができました。
![1000006115.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364428/e5301160-1a62-c4eb-72cb-11ecc04e61ce.png)


# 作成したプロダクト

私たちは「Sound Synapse」というWebアプリを開発しました。このアプリは、Spotify APIを使用して音楽の趣味が近い人をマッチングさせるものです。ユーザーは最大3つのお気に入りの曲を選択し、それらの曲情報を元に一つのベクトルを生成します。このベクトルを用いて、コサイン類似度に基づきユーザー間でマッチングを行います。
![スクリーンショット 2024-02-06 145418.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364428/4b945cd6-7495-1b7b-5881-ff8247321124.png)


# 技術構成

バックエンド: Docker, Flask,scikit-learn、Spotify API, OpenAI Embedding APIを使用しました。
フロントエンド: ReactとTypeScriptを使用しています。

# マッチング機能で使用した技術
このセクションでは、私の担当した、データの前処理、ベクトル化、類似度計算、およびレコメンデーション生成プロセスを簡略化して紹介します。
![スクリーンショット 2024-02-06 145314.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364428/7915f604-b9bd-3a87-3c4c-32961703fccd.png)


### データの前処理
まず、pandasライブラリを使用して、JSON形式のデータを読み込みます。このデータには、ユーザー名と曲名が含まれており、レコメンデーションシステムの基礎となります。
```
import pandas as pd
import json

#JSONファイルからテキストデータを読み込む
with open('japanese_music_data.json', 'r', encoding='utf-8') as file:
    data = json.load(file)["data"]
```
![スクリーンショット 2024-02-06 14.05.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364428/f347f55c-eb86-2180-fd93-8f628d71e290.png)

### ベクトルの生成
次に、OpenAIのAPIを用いて、各テキストデータからベクトルを生成します。このベクトル化プロセスでは、特定のモデルを指定してテキストの意味的な表現を数値の形で捉えます。ここでは、text-embedding-ada-002モデルを例に挙げます。
```
from openai import OpenAI

api_key = "YOUR_API_KEY"
client = OpenAI(api_key=api_key)

def get_embedding(text, model="text-embedding-ada-002"):
    response = client.embeddings.create(input=[text], model=model)
    return response.data[0].embedding
```
### データの更新
生成されたベクトルを使用してデータを更新します。ユーザー名と曲名の組み合わせからベクトルを生成し、それを元のデータに追加します。
```
updated_data = []
for item in data:
    text = item["user_name"] + " " + item["song_name"]
    vector = get_embedding(text)  # 注意: 実際にAPIを呼び出すにはAPIキーが必要
    updated_data.append({
        "user_name": item["user_name"],
        "song_name": item["song_name"],
        "vector": vector
    })
```
![スクリーンショット 2024-02-06 144838.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364428/493f5a44-7fba-0a5b-3666-8af2046d3521.png)

## レコメンデーションの生成
生成されたベクトルを用いて、コサイン類似度に基づくレコメンデーションを行います。このプロセスでは、各ユーザーに対して類似した曲を推薦します。
```
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
```

### ファイルから更新されたデータを読み込む
```
with open('updated_data.json', 'r', encoding='utf-8') as file:
    updated_data = json.load(file)['data']

vectors = np.array([item['vector'] for item in updated_data])
```

### コサイン類似度を計算し、ユーザーにマッチングしたユーザー名と曲情報を返します。
```
cosine_sim_matrix = cosine_similarity(vectors)

recommendations = []
for i, user_data in enumerate(updated_data):
    similarities = cosine_sim_matrix[i].copy()
    similarities[i] = 0
    top_3_indexes = np.argsort(similarities)[-3:][::-1]
    top_3_recommendations = [{"user_name": updated_data[idx]['user_name'], "song_name": updated_data[idx]['song_name']} for idx in top_3_indexes]
    recommendations.append({
        "user_name": user_data['user_name'],
        "song_name": user_data['song_name'],
        "recommendations": top_3_recommendations
    })
```
![スクリーンショット 2024-02-06 14.05.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364428/8b501cc2-947f-7e94-c9e9-2fe8fccf4abc.png)
# 感想

チーム内での非常に良いアイデアのもと、役割分担をしながらプロジェクトを進めた結果、多くの成長と学びがありました。特に、Spotify APIとEnbedding APIを用いた開発経験は、私にとって大きな成長の機会となりました。

# おわりに

今回のハッカソンは、専門領域の違う参加者が集まり、同世代のエンジニアたちと切磋琢磨できる素晴らしい機会でした。虎ノ門ヒルズでの開催ということもあり、素晴らしい景色と共に充実した時間を過ごすことができました。今回チャレンジできなかった部分も含め継続開発を行なっていきたいと思います。