본문 바로가기
Dot/iOS

iOS) CoreML을 이용한 트위터 감정 분석 앱 만들기

by jum0 2020. 9. 23.

CoreML을 이용한 트위터 감정 분석 앱 만들기입니다.

과정은 다음과 같습니다.

먼저 CreateML과 데이터셋을 이용해 자연어 처리 모델을 만들고, 트위터 API를 이용해 데이터(트윗)를 불러옵니다.

데이터는 JSON 형태로, 텍스트만 파싱한 후, 각 텍스트에 자연어 처리 모델을 적용해서 "Pos", "Neg", "Neutral" 중 예측합니다.

"Pos"는 +1점, "Neg"는 -1점, "Neutral"은 0점으로 계산하여 점수에 따라 이모티콘의 표정이 변하게 합니다.

< 순서 >

  • 자연어 처리 모델 만들기
  • 자연어 처리 모델  적용 및 트위터 API 앱에서 호출하기
    • 트위터 개발자 홈페이지에서 API_Key와 API_Secret_Key 받기
    • Swifter 및 자연어 처리 모델 앱에 추가하기
    • 자연어 처리 모델 확인하기
    • JSON 파싱 후, 데이터 batch 만들기 (SwifyJSON 사용)
    • 데이터 batch 자연어 처리 모델 적용하기
    • 점수 계산 및 UI 업데이트
    • 리팩토링
    • UX

< 자연어 처리 모델 만들기 >

먼저 모델을 만들기 위한 dataset을 준비합니다.

데이터는 이것으로 모델을 만들고 싶었으나, 컴퓨터가 버티지 못하는 것 같아서 흑ㅠ

애플에 관한 트위 데이터 했습니다. 

import Cocoa
import CreateML

let data = try MLDataTable(contentsOf: URL(fileURLWithPath: "/Users/파일 경로/twitter-sanders-apple3.csv"))

let (trainingData, testingData) = data.randomSplit(by: 0.8, seed: 5) // training 80% testing 20%

let sentimentClassifier = try MLTextClassifier(trainingData: trainingData, textColumn: "text", labelColumn: "class")

let evaluationMetrics = sentimentClassifier.evaluation(on: testingData, textColumn: "text", labelColumn: "class")

let evaluationAccuracy = (1.0 - evaluationMetrics.classificationError) *  100

let metadata = MLModelMetadata(author: "jum0", shortDescription: "A model trained to classify sentiment on Tweets", version: "1.0")

try sentimentClassifier.write(to: URL(fileURLWithPath: "/Users/파일 경로/TweetSentimentClassifier.mlmodel"))

try sentimentClassifier.prediction(from: "@Apple is crazy!")
try sentimentClassifier.prediction(from: "umm I don't think so.")
try sentimentClassifier.prediction(from: "I'm dying of love")

CreateML 프레임워크를 사용하여 먼저 데이터를 training data 80퍼센트, testing data 20퍼센트로 나누어 진행했습니다.

정확도는 대략 70퍼센트 정도여서 높지는 않습니다.

마지막에 모델을 저장할 때는 확장자인 .mlmodel로 해주시면 됩니다.

 

Create ML Tutorial: Getting Started

In this Create ML tutorial, you’ll learn how to transfer your learning to Turi Create, and gain familiarity with machine learning toolsets and terminology. No math needed!

www.raywenderlich.com

 

<자연어 처리 모델 적용 및 트위터 API 앱에서 호출하기>

<트위터 개발자 홈페이지에서 API_Key와 API_Secrect_Key 받기>

먼저 트위터 개발자 홈페이지에 들어가서 가입을 한 후, 프로젝트를 하나 만들어 API 키와 API secret 키를 받습니다.

 

Use Cases, Tutorials, & Documentation

Publish & analyze Tweets, optimize ads, & create unique customer experiences with the Twitter API, Twitter Ads API, & Twitter for Websites. Let's start building.

developer.twitter.com

<Swifter 및 자연어 처리 모델 앱에 추가하기>

다음으로 스위프트에서 트위터 API를 호출하기 위해서는 필요한 Swifter라는 프레임워크 적용입니다.

클론을 받은 후, 다음과 같이 Swifter.xcodeproj 파일을 최상위 바로 아래 위치시켜줍니다.

이후 SwifteriOS.framework를 추가해줍니다.

그리고는 자연어 처리 모델도 앱에 올려줍니다. 

이전 과정 중에서 어느 것을 먼저 해도 상관은 없습니다.

참고로 앱의 기본 UI는 다음과 같습니다.

먼저 트윗을 불러오는 코드만 짜서 보면 다음과 같습니다.

import UIKit
import SwifteriOS

class ViewController: UIViewController {

    @IBOutlet weak var backgroundView: UIView!
    @IBOutlet weak var sentimentLabel: UILabel!
    @IBOutlet weak var textField: UITextField!
    
    let swifter = Swifter(consumerKey: "API_Key", consumerSecret: "API_Secret_Key")
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        swifter.searchTweet(using: "@Apple", lang: "en", count: 100, tweetMode: .extended) { (results, metadata) in
            print(results)
        } failure: { (error) in
            print("There was an error with the Twitter API Request, \(error)")
        }

    }

    @IBAction func predictionPressed(_ sender: UIButton) {
        
    }
    
}

.searchTweet의 파라미터 정보는 document에서 확인할 수 있습니다.

결과는 JSON의 형태로 나타나는데, 콘솔 창으로 출력되는 결과를 JSON online editor 붙여 넣어서 그 구조를 파악할 수 있습니다.

<자연어 모델 확인하기>

다음은 아까 추가했던 자연어 처리 모델을 확인하는 작업입니다.

이후에 JSON 형태로 되어 있는 트윗에서 텍스트를 파싱한 후 각각 적용할 예정입니다.

import UIKit
import SwifteriOS

class ViewController: UIViewController {

    @IBOutlet weak var backgroundView: UIView!
    @IBOutlet weak var sentimentLabel: UILabel!
    @IBOutlet weak var textField: UITextField!
    
    let sentimentClassifier = TweetSentimentClassifier() // added
    
    let swifter = Swifter(consumerKey: "API_Key", consumerSecret: "API_Secret_Key")
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let prediction = try! sentimentClassifier.prediction(text: "@Apple is good.") // added
        print(prediction.label) // added
        
        swifter.searchTweet(using: "@Apple", lang: "en", count: 100, tweetMode: .extended) { (results, metadata) in
//            print(results)
        } failure: { (error) in
            print("There was an error with the Twitter API Request, \(error)")
        }

    }

    @IBAction func predictionPressed(_ sender: UIButton) {
        
    }
    
}

<JSON 파싱 후, 데이터 batch 만들기 (SwiftyJSON 사용)>

JSON 형식의 데이터를 쉽게 사용하기 위해 SwifyJSON 라이브러리를 이용합니다.

pod init을 통해 Podfile을 생성한 후, pod 'SwiftyJSON'을 적고 pod install을 합니다.

파싱된 데이터 중에서 첫 번째 결과만 살펴보는 코드는 다음과 같습니다. (if let은 옵셔널이기 때문에)

import UIKit
import SwifteriOS
import SwiftyJSON // added

class ViewController: UIViewController {

...
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
//        let prediction = try! sentimentClassifier.prediction(text: "@Apple is good.")
//        print(prediction.label)
        
        swifter.searchTweet(using: "@Apple", lang: "en", count: 100, tweetMode: .extended) { (results, metadata) in
            
            // added
            if let tweet = results[0]["full_text"].string {
                print(tweet)
            }
            
        } failure: { (error) in
            print("There was an error with the Twitter API Request, \(error)")
        }

    }

...
    
}

그리고는 Array을 만들어서 100개의 텍스트들을 저장해줍니다.

여기서 Classifier의 prediction을 적용하기 위해서는 타입이 TweetSentimentClassifierInput 되어야 하므로 convert 해서 넣어줍니다.

override func viewDidLoad() {
    super.viewDidLoad()
        
//    let prediction = try! sentimentClassifier.prediction(text: "@Apple is good.")
//    print(prediction.label)
        
    swifter.searchTweet(using: "@Apple", lang: "en", count: 100, tweetMode: .extended) { (results, metadata) in
            
        var tweets = [TweetSentimentClassifierInput]()
            
        for i in 0..<100 {
            if let tweet = results[i]["full_text"].string {
                let tweetForClassfication = TweetSentimentClassifierInput(text: tweet)
                tweets.append(tweetForClassfication)
            }
        }
                  
    } failure: { (error) in
        print("There was an error with the Twitter API Request, \(error)")
    }  
}

<데이터 batch 자연어 처리 모델 적용하기>

앞에서 확인했던 자연어 처리 모델을 적용해줍니다.

override func viewDidLoad() {
    super.viewDidLoad()
        
    swifter.searchTweet(using: "@Apple", lang: "en", count: 100, tweetMode: .extended) { (results, metadata) in
            
        var tweets = [TweetSentimentClassifierInput]()
            
        for i in 0..<100 {
            if let tweet = results[i]["full_text"].string {
                let tweetForClassfication = TweetSentimentClassifierInput(text: tweet)
                tweets.append(tweetForClassfication)
            }
        }
          
        // added
        do {
            let predictions = try self.sentimentClassifier.predictions(inputs: tweets)
                
            for pred in predictions {
                print(pred.label)
            }
                
        } catch {
            print("There was an error with making a prediction, \(error)")
        }
                 
    } failure: { (error) in
        print("There was an error with the Twitter API Request, \(error)")
    }
        
}

<점수 계산  및 UI 업데이트>

점수 계산을 위해서 sentimentScore라는 변수를 만들어서 숫자를 업데이트합니다.

do {
    let predictions = try self.sentimentClassifier.predictions(inputs: tweets)
                
    var sentimentScore = 0
                
    for pred in predictions {
        let sentiment = pred.label
                    
        if sentiment == "Pos" {
            sentimentScore += 1
        } else if sentiment == "Neg" {
            sentimentScore -= 1
        }
    }        
    print(sentimentScore)
                
    } catch {
        print("There was an error with making a prediction, \(error)")
}

기존에 "@Apple"이 들어갈 트윗의 검색을 텍스트 필드의 키워드로 대체하고, 버튼을 누르면 sentimentScore에 따라 이모티콘이 변하도록  코드를 predictionPressed 메서드 안으로 옮깁니다.

import UIKit
import SwifteriOS
import SwiftyJSON

class ViewController: UIViewController {
    
    @IBOutlet weak var backgroundView: UIView!
    @IBOutlet weak var sentimentLabel: UILabel!
    @IBOutlet weak var textField: UITextField!
    
    let sentimentClassifier = TweetSentimentClassifier()
    
   	let swifter = Swifter(consumerKey: "API_Key", consumerSecret: "API_Secret_Key")
    
    override func viewDidLoad() {
        super.viewDidLoad()
     
    }
    
    @IBAction func predictionPressed(_ sender: UIButton) {
        // 'if let' added
        if let searchText = textField.text {
            swifter.searchTweet(using: searchText, lang: "en", count: 100, tweetMode: .extended) { (results, metadata) in
                
                var tweets = [TweetSentimentClassifierInput]()
                
                for i in 0..<100 {
                    if let tweet = results[i]["full_text"].string {
                        let tweetForClassfication = TweetSentimentClassifierInput(text: tweet)
                        tweets.append(tweetForClassfication)
                    }
                }
                
                do {
                    let predictions = try self.sentimentClassifier.predictions(inputs: tweets)
                    
                    var sentimentScore = 0
                    
                    for pred in predictions {
                        let sentiment = pred.label
                        
                        if sentiment == "Pos" {
                            sentimentScore += 1
                        } else if sentiment == "Neg" {
                            sentimentScore -= 1
                        }
                    }
                    
                    // added
                    if sentimentScore > 20 {
                        self.sentimentLabel.text = "😍"
                    } else if sentimentScore > 10 {
                        self.sentimentLabel.text = "😄"
                    } else if sentimentScore > 0 {
                        self.sentimentLabel.text = "🙂"
                    } else if sentimentScore == 0 {
                        self.sentimentLabel.text = "😐"
                    } else if sentimentScore > -10 {
                        self.sentimentLabel.text = "😕"
                    } else if sentimentScore > -20 {
                        self.sentimentLabel.text = "😡"
                    } else {
                        self.sentimentLabel.text = "🤬"
                    }
                    
                } catch {
                    print("There was an error with making a prediction, \(error)")
                }
                
            } failure: { (error) in
                print("There was an error with the Twitter API Request, \(error)")
            }
        }
    }
    
}

<코드 리팩토링>

기능을 나누어서 메서드를 만듭니다. 메서드의 종류로는 트윗을 가져오는 메서드(fetchTweets), 데이터에 모델을 적용해 예측하는 메서드(makePrediction), 점수에 따라 UI를 업데이트하는 메서드로 나눕니다(updateUI)

import UIKit
import SwifteriOS
import SwiftyJSON

class ViewController: UIViewController {
    
    @IBOutlet weak var backgroundView: UIView!
    @IBOutlet weak var sentimentLabel: UILabel!
    @IBOutlet weak var textField: UITextField!
    
    let tweetCount = 100
    
    let sentimentClassifier = TweetSentimentClassifier()
    
    let swifter = Swifter(consumerKey: "API_Key", consumerSecret: "API_Secret_Key")
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
    
    @IBAction func predictionPressed(_ sender: UIButton) {
        fetchTweets()
    }
    
    func fetchTweets() {
        if let searchText = textField.text {
            swifter.searchTweet(using: searchText, lang: "en", count: tweetCount, tweetMode: .extended) { (results, metadata) in
                
                var tweets = [TweetSentimentClassifierInput]()
                
                for i in 0..<self.tweetCount {
                    if let tweet = results[i]["full_text"].string {
                        let tweetForClassification = TweetSentimentClassifierInput(text: tweet)
                        tweets.append(tweetForClassification)
                    }
                }
                
                self.makePrediction(with: tweets)
                
            } failure: { (error) in
                print("There was an error with the Twitter API Request, \(error)")
            }
        }
    }
    
    func makePrediction(with tweets: [TweetSentimentClassifierInput]) {
        
        do {
            let predictions = try self.sentimentClassifier.predictions(inputs: tweets)
            
            var sentimentScore = 0
            
            for pred in predictions {
                let sentiment = pred.label
                
                if sentiment == "Pos" {
                    sentimentScore += 1
                } else if sentiment == "Neg" {
                    sentimentScore -= 1
                }
            }
            
            updateUI(with: sentimentScore)
            
        } catch {
            print("There was an error with making a prediction, \(error)")
        }
    }
    
    func updateUI(with sentimentScore: Int) {
        
        print(sentimentScore)
        
        if sentimentScore > 20 {
            self.sentimentLabel.text = "😍"
        } else if sentimentScore > 10 {
            self.sentimentLabel.text = "😄"
        } else if sentimentScore > 0 {
            self.sentimentLabel.text = "🙂"
        } else if sentimentScore == 0 {
            self.sentimentLabel.text = "😐"
        } else if sentimentScore > -10 {
            self.sentimentLabel.text = "😕"
        } else if sentimentScore > -20 {
            self.sentimentLabel.text = "😡"
        } else {
            self.sentimentLabel.text = "🤬"
        }
    }
    
}

<UX>

부가적으로 간단하게 UX를 고려해서 키보드가 텍스트 입력 후 사라지는 기능을 추가합니다.

키보드는 IQKeyboardManager를 사용합니다.

사용 방법은 SwiftyJSON과 동일하게 Podfile에 추가하고 pod install을 한 후, AppDelegate.swift 파일에 다음 코드를 추가합니다.

import UIKit
import IQKeyboardManagerSwift // added

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        // added
        IQKeyboardManager.shared.enable = true
        IQKeyboardManager.shared.enableAutoToolbar = false
        IQKeyboardManager.shared.shouldResignOnTouchOutside = true
        
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }


}


수정해야 할 부분 있다면 알려주세요!

감사합니다!

반응형

댓글