【SwiftUI - SentimentScore】文章から感情分析できるアプリを作ってみた！

前回の投稿からお久しぶりです！
bravesoft株式会社でiOSエンジニアをしている永井です。

Visionフレームワークで<a href="https://qiita.com/bsj-r-nagai/items/5e450c0dc20cfbd989c4">Appleの機械学習から画像分類</a>というのをやりましたが、今回はVisionフレームワークにある「Natural Language - sentimentScore」についての感情分析アプリ作成の記事となります。

「ひとこと」呟いて、日記をつけられる感情日記アプリを作りたいと思っていたので調査内容を記載していきます。

## この記事で得られるもの
・sentimentScoreの使い方
・感情分析について

## SentimentScoreとは？
![スクリーンショット 2024-12-09 20.08.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796594/035f2a31-336d-e0b3-a9a6-51b33527403f.png)

sentimentScore は、テキストの感情を数値で表したものです。
-1 が「非常にネガティブ」、0 が「ニュートラル」、1 が「非常にポジティブ」を意味します。

※日本語非対応


**公式サイト**
Natural Language
https://developer.apple.com/jp/machine-learning/api/

SentimentScore
https://developer.apple.com/documentation/naturallanguage/nltagscheme/sentimentscore


## 実装

### 手順1 - 5段階に感情分析を分けるデザインを作成
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796594/58c6dabb-e09a-ad8f-8f08-89e55b49ef67.png>

Figmaで感情デザインを作成してみました！

レッド： 非常にネガティブ 
オレンジ： ネガティブ
イエロー： ニュートラル
イエローグリーン： ポジティブ
グリーン： 非常にポジティブ

上記の5段階でアプリ側に設定していきます。


### 手順2 -  感情分析のロジックを書いてみよう！
#### @Publishedの定義
```swift:ContentViewModel
class ContentViewModel: ObservableObject {
    @Published var inputText: String = ""
    @Published var sentimentScore: Double? = nil
}
```

テキスト入力に使うinputTextと感情スコアを入れるsentimentScoreを@Publishedで定義しましょう。


#### テキストから感情スコアを計算
```swift:ContentViewModel
func analyzeSentiment(for text: String) -> Double? {
    let tagger = NLTagger(tagSchemes: [.sentimentScore])
    tagger.string = text
        
    if let sentimentTag = tagger.tag(at: text.startIndex, unit: .paragraph, scheme: .sentimentScore).0,
        let score = Double(sentimentTag.rawValue) {
            return score
        }
        return nil
}
```

**NLTagger の初期化:**
NLTagger は自然言語処理ツールで、テキストにタグ（ラベル）を付けるために使います。ここでは .sentimentScore をタグスキームとして指定し、感情分析を行います。

**テキストの設定:**
tagger.string = text で解析するテキストを指定します。

 **感情スコアの取得:**
tagger.tag(at: text.startIndex, unit: .paragraph, scheme: .sentimentScore) で、テキストの先頭から段落単位で感情タグを取得します。

**感情タグを数値に変換:**
sentimentTag.rawValue から取得した感情スコア（文字列）を Double 型に変換しています。これが最終的な感情スコアになります。


#### 全体のコード
```swift:ContentViewModel
import SwiftUI
import NaturalLanguage

class ContentViewModel: ObservableObject {
    @Published var inputText: String = ""
    @Published var sentimentScore: Double? = nil
    
    // 「評価する」ボタンを押した時に
    func onTapEvaluationButton() {
        // 感情スコアの計算結果をsentimentScoreに代入
        sentimentScore = analyzeSentiment(for: inputText)
    }
    
    // テキストの感情スコアを計算する
    func analyzeSentiment(for text: String) -> Double? {
        let tagger = NLTagger(tagSchemes: [.sentimentScore])
        tagger.string = text
        
        if let sentimentTag = tagger.tag(at: text.startIndex, unit: .paragraph, scheme: .sentimentScore).0,
           let score = Double(sentimentTag.rawValue) {
            return score
        }
        return nil
    }
    
    // 感情スコアからカラーに変換する
    func classifySentimentToColor(score: Double) -> Color {
        switch score {
        case -1.0...(-0.6):
            return .red
        case -0.6..<(-0.2):
            return .orange
        case -0.2...0.2:
            return .yellow
        case 0.2..<0.6:
            return .green.opacity(0.5)
        case 0.6...1.0:
            return .green
        default:
            return .yellow
        }
    }
    
    // 感情スコアから画像に変換する
    func classifySentimentToImage(score: Double) -> String {
        switch score {
        case -1.0...(-0.6):
            return "veryNegative"
        case -0.6..<(-0.2):
            return "negative"
        case -0.2...0.2:
            return "neutral"
        case 0.2..<0.6:
            return "positive"
        case 0.6...1.0:
            return "veryPositive"
        default:
            return ""
        }
    }
    
    // 感情スコアから文字列に変換する
    func classifySentimentToString(score: Double) -> String {
        switch score {
        case -1.0...(-0.6):
            return "非常にネガティブ"
        case -0.6..<(-0.2):
            return "ネガティブ"
        case -0.2...0.2:
            return "ニュートラル"
        case 0.2..<0.6:
            return "ポジティブ"
        case 0.6...1.0:
            return "非常にポジティブ"
        default:
            return "スコア範囲外"
        }
    }
}
```


### 手順3 - テキスト入力と感情を表示するUIを用意しよう！
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796594/a10fe558-5a24-e582-7f5e-7a2c0e019805.png width = 50%>


```swift:ContentView
import SwiftUI

struct ContentView: View {
    @StateObject var viewModel = ContentViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Image("emotion")
            
            TextField("テキストを入力してください", text: $viewModel.inputText)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            
            Button("評価する") {
                viewModel.onTapEvaluationButton()
            }
            .foregroundColor(.blue)
            .padding()
            .disabled(viewModel.inputText.isEmpty)

            // スコアの結果があった場合に表示する
            if let score = viewModel.sentimentScore {
                // スコアの画像を表示
                Image(viewModel.classifySentimentToImage(score: score))

                // スコアのテキストを表示
                Text(viewModel.classifySentimentToString(score: score))
                    .font(.headline)
                    .foregroundColor(viewModel.classifySentimentToColor(score: score))
            }

            // 感情レベル表を表示
            Image("level")
                .resizable()
                .scaledToFit()
                .padding()
        }
    }
}
```




### 分析結果後の画面

**非常にネガティブ**
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796594/44d7017a-4c01-3ccb-6275-c31e6f55cf6c.png width = 50%>

英語: “I feel so lonely and sad today.”
日本語: 「今日はとても孤独で悲しい気持ちです。」

**ネガティブ**
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796594/083c92f1-61dc-b48e-42aa-f2f88124c4e4.png width = 50%>

英語: “It was fun at first, but it got boring in the second half.”
日本語: 「最初は楽しかったのに、後半はつまらなかったな。」


**ニュートラル**
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796594/4d137192-f592-1e96-eb80-4c3e0cf3ae7b.png width = 50%>

英語: “I had bread, eggs, and milk for breakfast.”
日本語: 「私は朝食にパンと卵と牛乳を食べました。」


**ポジティブ**
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796594/5abcc838-7ed8-aed5-2936-fcdb97bc76b2.png width = 50%>

英語: “I was able to finish a challenging project.”
日本語: 「難しいプロジェクトを終わらせられた。」

**非常にポジティブ**
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796594/484c320c-635d-0dbd-c58b-99ec9048eecb.png width = 50%>

英語: “I had a great time with my friends today!”
日本語: 「今日は友達と素晴らしい時間を過ごしました！」



無事に感情分析できましたね！
以上になります。

## 終わりに
今回の記事では、VisionフレームワークのNaturalLanguage - SentimentScore(感情分析)を紹介させていただきました。

実際分類してみると、英語しかまだ対応されていなかったり、文が短いと正しく分析されなかったりと色々と制約がありそうですね。今回の調査で、日本語対応されていなかったので別の感情分析ツールを使う必要があるかもしれません。いつの日か、日本語や他の言語でも対応する日が来るといいですね。

有名で広く使われている感情分析ツールは下記があるみたいです。
1. IBM Watson Natural Language Understanding
2. Google Cloud Natural Language API
3. Microsoft Azure Text Analytics API

機械学習によるオンデバイスAPIは「Vision」「Natural Language」「Speech」「Sound Anlysis」とまだまだ幅広くあり、どれもワクワクするような技術とAPIによってはアプリをさらに強化してくれるAPIが揃っているので、これからも記事を上げる際に、機械学習APIの使い方を発信できたらと思います。
#


<br>
bravesoftではiOSアプリやAndroidアプリの開発を行っています。
アプリ開発に興味がある方は是非是非、採用ページをご確認ください👀

https://recruit.jobcan.jp/bravesoft/show/b001/386578

https://www.bravesoft.co.jp/

## 参考資料

https://developer.apple.com/jp/machine-learning/api/

https://developer.apple.com/documentation/naturallanguage/nltagscheme/sentimentscore