├── Requirements.txt
├── Report.pdf
├── Screenshots
├── brain.png
├── ImageSample.png
├── crawl_images.PNG
├── crawl_texts.PNG
└── texts_exmpl.PNG
├── Scripts
├── Images_functions
│ ├── run_npz.py
│ ├── run_pexels.py
│ ├── run_save_images_crawler.py
│ ├── run_API_unsplash.py
│ ├── npz.py
│ ├── Save_images.py
│ ├── UnsplashAPI.py
│ └── pexels.py
└── Twitter_Crawler
│ ├── run_TWINT.py
│ └── Twint_pkge.py
├── data
├── TwitterP.py
├── Images_load.py
├── __init__.py
└── c3d.py
├── Notebooks
├── Remove_Duplicates.ipynb
├── Explore_Twint.ipynb
├── Bibliography.ipynb
└── TwitterscraperDemo.ipynb
├── models
└── bilstm.py
├── utils.py
├── README.md
├── LICENSE
├── ResNet-Transfer.ipynb
├── GLove+Bilstm.ipynb
├── ResNet.ipynb
└── Preprocessing_Texts.ipynb
/Requirements.txt:
--------------------------------------------------------------------------------
1 | pandas
2 | scikit-learn
3 | tqdm
4 | beautifulsoup4
--------------------------------------------------------------------------------
/Report.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BouzidiImen/Social_media_Prediction_depression/HEAD/Report.pdf
--------------------------------------------------------------------------------
/Screenshots/brain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BouzidiImen/Social_media_Prediction_depression/HEAD/Screenshots/brain.png
--------------------------------------------------------------------------------
/Screenshots/ImageSample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BouzidiImen/Social_media_Prediction_depression/HEAD/Screenshots/ImageSample.png
--------------------------------------------------------------------------------
/Screenshots/crawl_images.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BouzidiImen/Social_media_Prediction_depression/HEAD/Screenshots/crawl_images.PNG
--------------------------------------------------------------------------------
/Screenshots/crawl_texts.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BouzidiImen/Social_media_Prediction_depression/HEAD/Screenshots/crawl_texts.PNG
--------------------------------------------------------------------------------
/Screenshots/texts_exmpl.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BouzidiImen/Social_media_Prediction_depression/HEAD/Screenshots/texts_exmpl.PNG
--------------------------------------------------------------------------------
/Scripts/Images_functions/run_npz.py:
--------------------------------------------------------------------------------
1 | from npz import create_npz
2 | import numpy as np
3 | create_npz(path='Data/Unsplash_Pexels_Data/Depression/')
4 | create_npz(path='Data/Unsplash_Pexels_Data/Happiness/',label=np.array([0,1]))
5 |
--------------------------------------------------------------------------------
/Scripts/Images_functions/run_pexels.py:
--------------------------------------------------------------------------------
1 | from pexels import get_pexels_images
2 |
3 | DepressionKeywords = ['Suicide', 'Sad', 'Depression', 'Stress', 'Anxiety', 'Grief', 'Despair', 'Crying']
4 | HappyKeywords = ['Happy', 'excited']
5 | for key_word in HappyKeywords:
6 | get_pexels_images(key_word)
7 | #for key_word in DepressionKeywords:
8 | #get_pexels_images(key_word)
9 |
--------------------------------------------------------------------------------
/Scripts/Twitter_Crawler/run_TWINT.py:
--------------------------------------------------------------------------------
1 | from Twint_pkge import clean_data, get_profile_infos, get_timeline_by_username
2 | from tqdm import trange
3 | key_words = ["#lovemylife", "#lifeisgood", "#happyme"]
4 | data = clean_data(key_words)
5 | for i in trange(len(data)):
6 | get_profile_infos(data['username'].iloc[i])
7 | print('Okay profile'+str(i))
8 | get_timeline_by_username(data['username'].iloc[i])
9 |
--------------------------------------------------------------------------------
/Scripts/Images_functions/run_save_images_crawler.py:
--------------------------------------------------------------------------------
1 | from Save_images import save_images
2 | file_names_Deprssion_unsplash = ['Depression_unsplash']
3 | for file_name in file_names_Deprssion_unsplash:
4 | save_images(file_name=file_names_Deprssion_unsplash)
5 | file_names_Deprssion_Pexels = ['Depression_Pexels']
6 | for file_name in file_names_Deprssion_unsplash:
7 | save_images(website_name='Pexels_',file_name=file_name, folder_path='Pexels/Depressions/')
8 |
--------------------------------------------------------------------------------
/Scripts/Images_functions/run_API_unsplash.py:
--------------------------------------------------------------------------------
1 | from UnsplashAPI import get_unsplash_images
2 | import json
3 |
4 | with open('Token.json') as t:
5 | api_key = json.load(t)
6 | token = api_key[ 'unsplash' ]
7 | DepressionKeywords = [ 'Suicide', 'Sad', 'Depression', 'Stress', 'Anxiety', 'Grief', 'Despair', 'Crying' ]
8 | for key_word in DepressionKeywords:
9 | get_unsplash_images(token, key_word)
10 | DepressionKeywords = [ 'Suicide', 'Sad', 'Depression', 'Stress', 'Anxiety', 'Grief', 'Despair', 'Crying' ]
11 | for key_word in DepressionKeywords:
12 | get_unsplash_images(token, key_word)
13 |
--------------------------------------------------------------------------------
/Scripts/Images_functions/npz.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy as np
3 | import os
4 | from tqdm import tqdm
5 |
6 | DEFAULT_DATA_NAME = 'data.npz'
7 | DEFAULT_SCALE = .1
8 | DEFAULT_LABEL=np.array([1,0]) #for happiness changed to [0,1]
9 | DEFALUT_KEY='dep_'
10 |
11 | def _resize_to_np(filepath, label, scale):
12 | src = cv2.imread(filepath, cv2.IMREAD_UNCHANGED)
13 | width = int(src.shape[1] * scale)
14 | height = int(src.shape[0] * scale)
15 | output = cv2.resize(src, (width, height))
16 | img = np.array([output, label])
17 | return img.reshape((2, 1))
18 |
19 |
20 | def create_npz(path,key=DEFALUT_KEY, data_name=DEFAULT_DATA_NAME, scale=DEFAULT_SCALE,label=DEFAULT_LABEL):
21 | filepath = path + data_name
22 | if os.path.exists(filepath):
23 | print('Deleting existing data...')
24 | os.remove(filepath)
25 | pictures = os.listdir(path)
26 | all_imgs = []
27 | names=[]
28 | i = 0
29 | for pic in tqdm(pictures):
30 | all_imgs.append(_resize_to_np(path + pic, label, scale))
31 | names.append(key+str(i))
32 | i += 1
33 | print('all images were resized ')
34 | np.savez(filepath, **{name:value for name,value in zip(names,all_imgs)})
35 | print(f"Data saved into '{filepath}'")
36 |
--------------------------------------------------------------------------------
/Scripts/Images_functions/Save_images.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | from requests import get
3 | from tqdm import trange
4 | import time
5 | from socket import error as socket_error
6 | import errno
7 | import pathlib
8 | from random import randint
9 | from urllib.request import urlcleanup
10 |
11 | DEFAULT_FILE_NAME = 'Suicide'
12 | DEFAULT_FOLDER_PATH = 'Unsplash/Depression/'
13 | DEFAULT_NAME = 'Unsplash_'
14 |
15 | headers = {
16 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Cafari/537.36'}
17 |
18 |
19 | def _save_image(website_name,Name, Extension, Link, path_to_folder):
20 | try:
21 | name = website_name + Name
22 | pic = get(Link, headers=headers)
23 | with open(path_to_folder + name + "." + Extension, 'wb') as photo:
24 | photo.write(pic.content)
25 | except socket_error as e:
26 | if e.errno != errno.ECONNRESET:
27 | raise
28 | urlcleanup()
29 |
30 |
31 | def save_images(website_name=DEFAULT_NAME,file_name=DEFAULT_FILE_NAME, folder_path=DEFAULT_FOLDER_PATH):
32 | missed = []
33 | data = pd.read_csv(file_name + ".csv")
34 | pathlib.Path(folder_path).mkdir(parents=True, exist_ok=True)
35 | for i in trange(len(data)):
36 | wait = randint(5, 15)
37 | time.sleep(wait)
38 | print(f'\nWaiting {wait}s...')
39 | try:
40 | _save_image(website_name,data['Name'].iloc[i], data['Extension'].iloc[i], data['Links'].iloc[i], folder_path)
41 | except:
42 | missed.append(data['Links'].iloc[i])
43 | missed_data = pd.DataFrame({'Missed_Links': missed}, index=None)
44 | missed_data.to_csv('MissedData.csv')
45 |
--------------------------------------------------------------------------------
/data/TwitterP.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import numpy as np
3 | import json
4 | import os
5 | from tqdm import tqdm
6 | from utils import maybe_download_and_extract, getlink
7 |
8 | SOURCE_URL = "http://www.mediafire.com/file/xp2jp8ezm3ynpc1/Twitter_Data%25282%2529.zip/file"
9 | DATA_DEFAULT_PATH = '~/.datasets/Crawled-Twitter-Data/'
10 | DEFAULT_TEST_SIZE = .25
11 | DEFAULT_SEED = 2020
12 |
13 |
14 | def load_data(data_path=DATA_DEFAULT_PATH, link=SOURCE_URL, test_size=DEFAULT_TEST_SIZE):
15 | """
16 | load data from website and return train, test and validation data
17 | :param data_path where data will be saved
18 | :param link to data
19 | :param test_size
20 |
21 | """
22 | data_path = os.path.expanduser(data_path)
23 | # Download files
24 | maybe_download_and_extract(getlink(link), data_path)
25 | # read data
26 | Twitter_data = pd.read_csv(data_path + "Twitter_data/Profiles.csv")
27 | L = len(Twitter_data)
28 | train, val, test = np.split(Twitter_data, [ int(L * (1 - test_size) * (1 - test_size)), int(L * (1 - test_size)) ])
29 | return train, val, test
30 |
31 |
32 | def get_username_profile(username, data_path=DATA_DEFAULT_PATH):
33 | """
34 | load user's profile for a given username
35 | :param data_path where data is saved
36 | :param username
37 |
38 | """
39 | timelines = os.listdir(data_path + 'Twitter_data/Timelines')
40 | clean_usernames = [s.strip('.csv') for s in timelines]
41 | for i in range(len(clean_usernames)):
42 | if clean_usernames[i] == username:
43 | return (pd.read_csv(data_path + 'Twitter_data/Timelines/' + username + '.csv'))
44 | return (print("user's timeline not found"))
45 |
--------------------------------------------------------------------------------
/Scripts/Images_functions/UnsplashAPI.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 | import requests as requests
3 | from urllib.request import urlretrieve
4 | from tqdm import trange
5 | import time
6 | from socket import error as socket_error
7 | import errno
8 |
9 | DEFAULT_KEY_WORD = 'depression'
10 | DEFAULT_PATH = "Unsplash/Depression/"
11 |
12 | def get_unsplash_images(token, key_word=DEFAULT_KEY_WORD, path_to_folder=DEFAULT_PATH):
13 | """
14 | Using Unsplash API to save url of photos in a csv
15 | :param path_to_folder: Path there images will be saved
16 | :param browser: webdriver
17 | :param key_word: key word to be searched
18 | :param token
19 |
20 | """
21 | r = requests.get(f'https://api.unsplash.com/search/photos?query={key_word}&client_id={token}')
22 | infos = r.json()
23 | total_pages = infos['total_pages']
24 | total_images = infos['total']
25 | links = []
26 | num_per_page = 200
27 | for pg in range(1, total_pages + 1):
28 | new_r = requests.get(f'https://api.unsplash.com/search/photos?query={key_word}' +
29 | f'&page={pg}&per_page={num_per_page}&client_id={token}')
30 | data = new_r.json()
31 | for img_data in data['results']:
32 | img_url = img_data['urls']['raw']
33 | links.append(img_url)
34 | if len(links) == total_images:
35 | print('Links for all images')
36 | else:
37 | print('Missing links')
38 | Clean_links = []
39 | for l in links:
40 | Clean_links.append(l.split('?')[0])
41 | pathlib.Path(path_to_folder).mkdir(parents=True, exist_ok=True)
42 | try:
43 | for i in trange(len(Clean_links)):
44 | name = 'Unsplash_' + key_word + '_' + str(i)
45 | time.sleep(5)
46 | urlretrieve(links[i], path_to_folder+ name + "." + 'jpeg')
47 | except socket_error as e:
48 | if e.errno != errno.ECONNRESET:
49 | raise
50 | pass
51 |
--------------------------------------------------------------------------------
/Scripts/Twitter_Crawler/Twint_pkge.py:
--------------------------------------------------------------------------------
1 | import twint
2 | import pandas as pd
3 | from tqdm import trange
4 | import sys, os
5 |
6 | DEFAULT_KEYWORD = "I suffer from depression"
7 | DEFAULT_KEYWORDS = [ "I am diagnosed with depression", 'I am fighting depression', 'I suffer from depression' ]
8 | DEFAULT_LIMIT = 3000
9 | DEFAULT_USERNAME = 'Imen'
10 | DEFAULT_LIMIT_PROFILE_STATUS = 100
11 |
12 |
13 | def clean_data(key_words=DEFAULT_KEYWORDS):
14 | data = pd.DataFrame()
15 | for key in key_words:
16 | data = data.append(pd.read_csv(key + ".csv"))
17 | data = data.drop_duplicates(subset=['user_id'], keep='first')
18 | return data
19 |
20 |
21 | def get_keywords_tweets(keyword=DEFAULT_KEYWORD, limit=DEFAULT_LIMIT):
22 | """
23 | This function returns a csv data set with tweets containing the keyword
24 | :param keyword is the key word to search for
25 | :param limit number of tweets to retrieve
26 | """
27 | c = twint.Config()
28 | c.Search = keyword
29 | c.Limit = limit
30 | c.Store_csv = True
31 | c.Output = keyword + ".csv"
32 | sys.stdout = open(os.devnull, 'w')
33 | twint.run.Search(c)
34 |
35 |
36 | def get_profile_infos(user_name=DEFAULT_USERNAME):
37 | """
38 | This function returns a csv file with users pesonal information (bio/location/followers/following)
39 | :param user_name Username of a twitter user
40 | """
41 | c = twint.Config()
42 | c.Username = user_name
43 | c.Store_csv = True
44 | c.Output = ("Profileinfos.csv")
45 | sys.stdout = open(os.devnull, 'w')
46 | twint.run.Lookup(c)
47 |
48 |
49 | def get_timeline_by_username(user_name=DEFAULT_USERNAME, limit=DEFAULT_LIMIT_PROFILE_STATUS):
50 | """
51 | This function returns csv files each contain 100 recent post of a user
52 | :param user_name user_name Username of a twitter user
53 | """
54 | c = twint.Config()
55 | c.Username = user_name
56 | c.Profile = True
57 | c.Retweets = True
58 | c.Limit = limit
59 | c.Store_csv = True
60 | c.Output = ('Timelines/' + user_name + ".csv")
61 | sys.stdout = open(os.devnull, 'w')
62 | twint.run.Search(c)
63 |
--------------------------------------------------------------------------------
/data/Images_load.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import os
3 | from tqdm import trange
4 | from utils import maybe_download_and_extract, getlink
5 | from data import DataSet, DataSets
6 |
7 | SOURCE_NEGATIVE_URL = "http://www.mediafire.com/file/17l0aqdbqhbvlfu/negative.npz/file"
8 | SOURCE_POSITIVE_URL = "http://www.mediafire.com/file/v1s3tqqzriuuq61/positive.npz/file"
9 | DATA_DEFAULT_PATH = '~/.datasets/Images_From_Unsplash_And_Pexels/'
10 | DEFAULT_TEST_SIZE = .25
11 | DEFAULT_SEED=2020
12 |
13 |
14 | def _extract(Pdata, Ndata):
15 | '''
16 | Extract features and labels of images
17 | :param Pdata: data for depressed users' images
18 | :param Ndata: data for non depressed users' images
19 | :return: features and labels
20 | '''
21 | # Extract features and labels
22 | features = list(Pdata[ Pdata.files[ 0 ] ][ 0 ])
23 | labels = list(Pdata[ Pdata.files[ 0 ] ][ 1 ])
24 | for i in trange(1,len(Pdata)):
25 | features.append(Pdata[ Pdata.files[ i ] ][ 0 ][ 0 ])
26 | labels.append(Pdata[ Pdata.files[ i ] ][ 1 ][ 0 ])
27 | for i in trange(len(Ndata)):
28 | features.append(Ndata[ Ndata.files[ i ] ][ 0 ][ 0 ])
29 | labels.append(Ndata[ Ndata.files[ i ] ][ 1 ][ 0 ])
30 | return features, labels
31 |
32 |
33 | def load_data(data_path=DATA_DEFAULT_PATH, seed=DEFAULT_SEED, test_size=DEFAULT_TEST_SIZE):
34 | """
35 | Loads dataset.
36 | :param data_path: string
37 | the path of the directory that contains the dataset
38 | """
39 |
40 | data_path = os.path.expanduser(data_path)
41 |
42 | # Download files
43 | maybe_download_and_extract(getlink(SOURCE_NEGATIVE_URL), data_path)
44 | maybe_download_and_extract(getlink(SOURCE_POSITIVE_URL), data_path)
45 | # read data
46 | P = np.load(data_path + 'positive.npz',allow_pickle=True) # load data for depressed users
47 | N = np.load(data_path + 'negative.npz',allow_pickle=True) # load data for not depressed users
48 | features, labels = _extract(P, N)
49 |
50 | data = DataSet(features, labels, seed=seed)
51 | train, test = data.split(split_size=test_size)
52 | train, validation = train.split(split_size=test_size)
53 | return DataSets(train, validation, test)
54 |
--------------------------------------------------------------------------------
/Notebooks/Remove_Duplicates.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "### To remove duplicate images using fdupes for linux \n",
8 | "* fdupes is a Linux utility for identifying or deleting duplicate files by comparing md5sum then running a byte-to-byte comparaison"
9 | ]
10 | },
11 | {
12 | "cell_type": "markdown",
13 | "metadata": {},
14 | "source": [
15 | "### Install fdupes for linux "
16 | ]
17 | },
18 | {
19 | "cell_type": "code",
20 | "execution_count": null,
21 | "metadata": {},
22 | "outputs": [],
23 | "source": [
24 | "sudo apt-get update && apt-get install fdupes "
25 | ]
26 | },
27 | {
28 | "cell_type": "markdown",
29 | "metadata": {},
30 | "source": [
31 | " ### Search duplicate photos in a folder "
32 | ]
33 | },
34 | {
35 | "cell_type": "code",
36 | "execution_count": null,
37 | "metadata": {},
38 | "outputs": [],
39 | "source": [
40 | "fdupes Path_To_folder"
41 | ]
42 | },
43 | {
44 | "cell_type": "markdown",
45 | "metadata": {},
46 | "source": [
47 | "### Number of duplicates in a folder "
48 | ]
49 | },
50 | {
51 | "cell_type": "code",
52 | "execution_count": null,
53 | "metadata": {},
54 | "outputs": [],
55 | "source": [
56 | "fdupes -m Path_To_folder"
57 | ]
58 | },
59 | {
60 | "cell_type": "markdown",
61 | "metadata": {},
62 | "source": [
63 | "### Delete files and preserve the first one"
64 | ]
65 | },
66 | {
67 | "cell_type": "code",
68 | "execution_count": null,
69 | "metadata": {},
70 | "outputs": [],
71 | "source": [
72 | "fdupes -dN Path_To_folder"
73 | ]
74 | }
75 | ],
76 | "metadata": {
77 | "kernelspec": {
78 | "display_name": "Python 3",
79 | "language": "python",
80 | "name": "python3"
81 | },
82 | "language_info": {
83 | "codemirror_mode": {
84 | "name": "ipython",
85 | "version": 3
86 | },
87 | "file_extension": ".py",
88 | "mimetype": "text/x-python",
89 | "name": "python",
90 | "nbconvert_exporter": "python",
91 | "pygments_lexer": "ipython3",
92 | "version": "3.6.10"
93 | }
94 | },
95 | "nbformat": 4,
96 | "nbformat_minor": 4
97 | }
98 |
--------------------------------------------------------------------------------
/data/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import collections
3 | import numpy as np
4 | from sklearn.model_selection import train_test_split
5 |
6 | DataSets = collections.namedtuple('DataSets', ['train', 'validation', 'test'])
7 | class DataSet(object):
8 | def __init__(self, features, labels, seed=None, name=None):
9 | if name is None:
10 | name = self.__class__.__name__
11 | self._logger = logging.getLogger(self.__class__.__name__)
12 | self._features = features
13 | self._labels = labels
14 | self._seed = seed
15 | self._name = name
16 |
17 | self._epoch_completed = 0
18 | self._index_in_epoch = 0
19 |
20 |
21 | @property
22 | def features(self):
23 | return self._features
24 |
25 | @property
26 | def labels(self):
27 | return self._labels
28 |
29 | @property
30 | def num_examples(self):
31 | return len(self._features)
32 |
33 | @property
34 | def input_dim(self):
35 | if len(self._features.shape) == 2:
36 | return self._features.shape[1]
37 | else:
38 | return self._features.shape[1:]
39 |
40 | @property
41 | def output_dim(self):
42 | return self.n_classes
43 |
44 | @property
45 | def n_classes(self):
46 | if len(self.labels.shape) == 1:
47 | _n_classes = len(np.unique(self.labels))
48 | else:
49 | _n_classes = self.labels.shape[1]
50 | return _n_classes
51 |
52 | def split(self, split_size):
53 | assert split_size > 0 and not split_size > 1
54 | features_train, features_test, labels_train, labels_test = train_test_split(self._features, self._labels,
55 | test_size=split_size,
56 | random_state=self._seed)
57 |
58 | train = DataSet(features_train, labels_train, seed=self._seed)
59 | test = DataSet(features_test, labels_test, seed=self._seed)
60 | return train, test
61 |
62 | def shuffle(self):
63 | idx = np.arange(0, self.num_examples)
64 | np.random.seed(self._seed)
65 | np.random.shuffle(idx)
66 | self._features = self.features[idx]
67 | self._labels = self.labels[idx]
--------------------------------------------------------------------------------
/data/c3d.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import numpy as np
3 | import json
4 | import os
5 | from tqdm import tqdm
6 | import logging
7 | from utils import maybe_download_and_extract
8 | from data import DataSet, DataSets
9 | from utils import maybe_download_and_extract, getlink
10 |
11 | SOURCE_URL = "http://www.mediafire.com/file/33gw4n73pyoa2ea/data.zip/file"
12 | DATA_DEFAULT_PATH = '~/.datasets/Cross-Domain_Depression_Detection_via Harvesting_Social_Media/'
13 | DEFAULT_TEST_SIZE=.25
14 | DEFAULT_SEED=2020
15 |
16 | def _read_data(data_path):
17 | '''
18 | Read downloaded data
19 | :param data_path: path for the dataset json files
20 | :return: tweets and labels
21 | '''
22 | # Import data
23 | tweets=[]
24 | labels=[]
25 | labels_path = data_path+'data/'
26 | for cathegory in os.listdir(labels_path):
27 | data_files=labels_path+cathegory
28 | files = os.listdir(data_files)
29 | for file in tqdm(files):
30 | with open(data_files+'/'+file) as json_d:
31 | tmp = json.load(json_d)
32 | tweets.append(tmp['text'])
33 | if cathegory=='negative':
34 | labels.append(0)
35 | if cathegory=='positive':
36 | labels.append(0)
37 | x, y = np.array(tweets), np.array(labels)
38 | return x, y
39 |
40 |
41 | def load_data(data_path=DATA_DEFAULT_PATH, test_size=DEFAULT_TEST_SIZE, seed=DEFAULT_SEED):
42 | """
43 | Loads dataset.
44 |
45 | Args:
46 | data_path: string
47 | the path of the directory that contains the dataset
48 | test_size: float
49 | Value between 0 and 1 that indicated the proportion to use for the test set. This is calculated from
50 | the train set.
51 | seed: integer
52 | initialization of the random number generator
53 | Returns:
54 | DataSets object
55 | A named tuple of type Datasets containing the train and test sets all of them of type dataset.
56 | """
57 |
58 | data_path = os.path.expanduser(data_path)
59 |
60 | # Download files
61 | maybe_download_and_extract(getlink(SOURCE_URL), data_path)
62 |
63 | # read data to memory
64 | x, y = _read_data(data_path)
65 |
66 | data = DataSet(x, y, seed=seed)
67 | train, test = data.split(split_size=test_size)
68 | train, validation= train.split(split_size=test_size)
69 | return DataSets(train,validation,test)
70 |
--------------------------------------------------------------------------------
/Scripts/Images_functions/pexels.py:
--------------------------------------------------------------------------------
1 | from selenium import webdriver
2 | import time
3 | from tqdm import trange
4 | import urllib.request
5 | from socket import error as socket_error
6 | import errno
7 | import pathlib
8 |
9 |
10 | DEFAULT_key_word = "Depression"
11 | DEFAULT_PATH_FOLDER = "Pexels/Happy/"
12 | DEFAULT_WEB_DRIVER = webdriver.Firefox()
13 | DEFAULT_TIME_SLEEP_SCROLL = 5
14 |
15 |
16 | class AppURLopener(urllib.request.FancyURLopener):
17 | version = "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.69 " \
18 | "Safari/537.36 "
19 |
20 |
21 | def _choose(messy):
22 | clean = []
23 | for elt in messy:
24 | link = elt.get_attribute('src')
25 | if not (link.find('https://images.pexels.com/photos/')):
26 | clean.append(link.split("?")[0])
27 | return clean
28 | def _scroll_down(browser, time_sleep):
29 | """A method for scrolling the page."""
30 | # Get scroll height.
31 | last_height = browser.execute_script("return document.body.scrollHeight")
32 | while True:
33 | # Scroll down to the bottom.
34 | browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
35 | # Wait to load the page, this depends on the speed of internet connection
36 | time.sleep(time_sleep)
37 | # Calculate new scroll height and compare with last scroll height.
38 | new_height = browser.execute_script("return document.body.scrollHeight")
39 | if new_height == last_height:
40 | break
41 | last_height = new_height
42 |
43 | def get_pexels_images(key_word=DEFAULT_key_word, browser=DEFAULT_WEB_DRIVER, path_to_folder=DEFAULT_PATH_FOLDER, time_sleep_for_scroll = DEFAULT_TIME_SLEEP_SCROLL):
44 | """
45 | Crawl Pexel Website and download all images with a key word
46 | :param path_to_folder: Path there images will be saved
47 | :param browser: webdriver
48 | :param key_word: key word to be searched
49 |
50 | """
51 | pathlib.Path(path_to_folder).mkdir(parents=True, exist_ok=True)
52 | browser.get("https://www.pexels.com/search/" + key_word)
53 | _scroll_down(browser, time_sleep_for_scroll)
54 | browser.implicitly_wait(10) # seconds
55 | columns = browser.find_elements_by_class_name('photos__column')
56 | all_imgs = []
57 | for column in columns:
58 | all_imgs.append(column.find_elements_by_tag_name("img"))
59 | imgs = []
60 | for col in all_imgs:
61 | for img in col:
62 | imgs.append(img)
63 | img_links = _choose(imgs)
64 | try: # Because an exception occurred while running the code (three times)
65 | urllib_urlopener = AppURLopener()
66 | for i in trange(len(img_links)):
67 | extension = img_links[i].split('.')[-1]
68 | name = key_word + str(i)
69 | time.sleep(5)
70 | urllib_urlopener.retrieve(img_links[i], path_to_folder + name + "." + extension)
71 | except socket_error as e:
72 | if e.errno != errno.ECONNRESET:
73 | raise
74 | pass
75 | browser.close()
76 |
--------------------------------------------------------------------------------
/models/bilstm.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | from keras.preprocessing.text import Tokenizer
4 | from keras.preprocessing.sequence import pad_sequences
5 | from keras.models import Sequential
6 | from keras.layers import Dense, Embedding, LSTM, Dropout, Bidirectional
7 | from keras.utils.np_utils import to_categorical
8 | from keras.optimizers import Adam
9 | from keras.callbacks import EarlyStopping, ModelCheckpoint
10 | import os
11 |
12 | DEFAULT_MAX_FEATURES = 2000
13 | DEFAULT_MAX_LENGTH = 28
14 | DEFAULT_EMBED = 128
15 | DEFAULT_LSTM_UNITS = 400
16 | DEFAULT_BATCH = 300
17 | DEFAULT_EPOCHS = 200
18 | DEFAULT_LR = .001
19 | DEFAULT_PATIENCE=10
20 |
21 |
22 | def _preprocess_data(train,validation, test, max_features=DEFAULT_MAX_FEATURES, max_len=DEFAULT_MAX_LENGTH):
23 | """
24 | Prepare data sequentially to feed it to the neural network
25 | :param train:
26 | train data
27 | :param test:
28 | test data
29 | :param max_features:
30 | maximum number that sentence may contain
31 | :param max_len:
32 | padding size
33 | :return:
34 | train and test, features and their labels
35 | """
36 | train_tokenizer = Tokenizer(num_words=max_features, split=' ')
37 | train_tokenizer.fit_on_texts(train.features)
38 | x_train = train_tokenizer.texts_to_sequences(train.features)
39 | x_train = pad_sequences(x_train, maxlen=max_len)
40 |
41 | test_tokenizer = Tokenizer(num_words=max_features, split=' ')
42 | test_tokenizer.fit_on_texts(test.features)
43 | x_test = test_tokenizer.texts_to_sequences(test.features)
44 | x_test = pad_sequences(x_test, maxlen=max_len)
45 |
46 | validation_tokenizer = Tokenizer(num_words=max_features, split=' ')
47 | validation_tokenizer.fit_on_texts(validation.features)
48 | x_validation = validation_tokenizer.texts_to_sequences(validation.features)
49 | x_validation = pad_sequences(x_validation, maxlen=max_len)
50 |
51 | y_train = to_categorical(train.labels)
52 | y_test = to_categorical(test.labels)
53 | y_validation = to_categorical(validation.labels)
54 |
55 | return x_train, y_train, x_test, y_test, x_validation, y_validation
56 |
57 |
58 | def model(train, validation, test, embed_dim=DEFAULT_EMBED, lstm_units=DEFAULT_LSTM_UNITS, batch_size=DEFAULT_BATCH,
59 | lr=DEFAULT_LR,patience=DEFAULT_PATIENCE,epochs=DEFAULT_EPOCHS, max_features=DEFAULT_MAX_FEATURES, max_len=DEFAULT_MAX_LENGTH):
60 | """
61 | LSTM MODEL FOR BINARY CLASSIFICATION
62 | :param train:
63 | train data
64 | :param validation
65 | validation data
66 | :param test:
67 | test data
68 | :param embed_dim:
69 | embedding dimension
70 | :param lstm_units:
71 | number of units in an lstm cell
72 | :param batch_size:
73 | batch size
74 | :param epochs:
75 | number of epochs
76 | :param max_features:
77 | maximum number that sentence may contain
78 | :param max_len:
79 | padding size
80 | :return:
81 | """
82 | adam = Adam(lr=lr, beta_1=0.9, beta_2=0.999, epsilon=None, decay=0.0, amsgrad=False)
83 |
84 | file_path = "weights-improvement-{epoch:02d}-{val_acc:.2f}.hdf5"
85 |
86 | check_point = ModelCheckpoint(file_path, monitor='val_loss', verbose=1, save_best_only=True,
87 | save_weights_only=True, mode='auto', period=1)
88 | early_stop = EarlyStopping(monitor='val_loss', patience=patience, verbose=1,
89 | mode='auto', restore_best_weights=True)
90 |
91 | out_dim = len(np.unique(train.labels))
92 | x_train, y_train, x_test, y_test, x_validation, y_validation = _preprocess_data(train, validation, test, max_features, max_len)
93 | loss = 'binary_crossentropy'
94 | if y_train.shape[1]>2:
95 | loss = 'categorical_crossentropy'
96 |
97 | model = Sequential()
98 | model.add(Embedding(max_features, embed_dim, input_length=x_train.shape[1]))
99 | model.add(Dropout(.2))
100 | model.add(Bidirectional(LSTM(lstm_units, dropout=.8, recurrent_dropout=.8))=
101 | model.add(Dropout(.8))
102 | model.add(Dense(out_dim, activation='softmax'))
103 | model.compile(loss=loss, optimizer=adam, metrics=['accuracy'])
104 | model.fit(x_train, y_train, batch_size,epochs, verbose=True,
105 | validation_data=(x_validation,y_validation),
106 | callbacks=[check_point,early_stop])
107 | loss, train_accuracy = model.evaluate(x_train, y_train, verbose=False)
108 | print("Training Accuracy: {:.4f}".format(train_accuracy))
109 | loss, test_accuracy = model.evaluate(x_test, y_test, verbose=False)
110 | print("Testing Accuracy: {:.4f}".format(test_accuracy))
111 | return train_accuracy,test_accuracy
112 |
--------------------------------------------------------------------------------
/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import urllib.request
4 | from bs4 import BeautifulSoup as soup
5 | import zipfile
6 | import tarfile
7 | import logging
8 |
9 | def maybe_download(file_name, data_path, url):
10 | """
11 | Download file from url if not found.
12 |
13 | This function will check if the data_path directory exists otherwise it will create it. it will check if file_name
14 | exists in data_path directory otherwise it will download it from url.
15 |
16 | Args:
17 | file_name: string
18 | The name of file after download
19 | data_path: string
20 | The folder where where data should be downloaded
21 | url: string
22 | The url of the file to download
23 |
24 | Returns:
25 | string
26 | the name of the downloaded file
27 |
28 | """
29 | logger = logging.getLogger(__name__ + '.maybe_download')
30 |
31 | file_path = data_path + file_name
32 | logger.debug(('Checking {} into {}'.format(file_name, data_path)))
33 |
34 | # Check data dir exists
35 | if not os.path.exists(data_path):
36 | logger.debug('Folder {} not found, creating it'.format(data_path))
37 | os.makedirs(data_path)
38 |
39 | # Check data file exists
40 | if os.path.exists(file_path):
41 | logger.debug('File {} found'.format(file_path))
42 | return file_path
43 |
44 | # Otherwise download it
45 | logger.info('Downloading file {} from {}'.format(file_path, url))
46 | temp_file_name, _ = urllib.request.urlretrieve(url, file_path)
47 | logger.info('Successfully downloaded file {}, {} bites'.format(temp_file_name, os.stat(temp_file_name).st_size))
48 |
49 | return file_path
50 |
51 |
52 | def _print_download_progress(count, block_size, total_size):
53 | """
54 | Function used for printing the download progress.
55 | Used as a call-back function in maybe_download_and_extract().
56 | """
57 |
58 | # Percentage completion.
59 | pct_complete = float(count * block_size) / total_size
60 |
61 | # Limit it because rounding errors may cause it to exceed 100%.
62 | pct_complete = min(1.0, pct_complete)
63 |
64 | # Status-message. Note the \r which means the line should overwrite itself.
65 | msg = "\r- Download progress: {0:.1%}".format(pct_complete)
66 |
67 | # Print it.
68 | sys.stdout.write(msg)
69 |
70 |
71 | sys.stdout.flush()
72 |
73 |
74 | def maybe_download_and_extract(url, download_dir):
75 | """
76 | Download and extract the data if it doesn't already exist.
77 | Assumes the url is a tar-ball file.
78 | :param url:
79 | Internet URL for the tar-file to download.
80 | Example: "https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz"
81 | :param download_dir:
82 | Directory where the downloaded file is saved.
83 | Example: "data/CIFAR-10/"
84 | :return:
85 | Nothing.
86 | """
87 |
88 | # Filename for saving the file downloaded from the internet.
89 | # Use the filename from the URL and add it to the download_dir.
90 | filename = url.split('/')[-1]
91 | filename = filename.split('?')[0]
92 | file_path = os.path.join(download_dir, filename)
93 |
94 | # Check if the file already exists.
95 | # If it exists then we assume it has also been extracted,
96 | # otherwise we need to download and extract it now.
97 | if not os.path.exists(file_path):
98 | # Check if the download directory exists, otherwise create it.
99 | if not os.path.exists(download_dir):
100 | os.makedirs(download_dir)
101 |
102 | # Download the file from the internet.
103 | file_path, _ = urllib.request.urlretrieve(url=url,
104 | filename=file_path,
105 | reporthook=_print_download_progress)
106 |
107 | print()
108 | print("Download finished. Extracting files.")
109 |
110 | if file_path.endswith(".zip"):
111 | # Unpack the zip-file.
112 | zipfile.ZipFile(file=file_path, mode="r").extractall(download_dir)
113 | elif file_path.endswith((".tar.gz", ".tgz")):
114 | # Unpack the tar-ball.
115 | tarfile.open(name=file_path, mode="r:gz").extractall(download_dir)
116 |
117 | print("Done.")
118 | else:
119 | print("Data has apparently already been downloaded and unpacked.")
120 |
121 | def getlink(link):
122 | driver = urllib.request.urlopen(link)
123 | content = driver.read()
124 | driver.close()
125 | page = soup(content, "html.parser")
126 | download = page.find("div",{"class":"download_link"})
127 | return download.find("a",{"class":"input"})['href']
--------------------------------------------------------------------------------
/Notebooks/Explore_Twint.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Twint Package"
8 | ]
9 | },
10 | {
11 | "cell_type": "code",
12 | "execution_count": 1,
13 | "metadata": {},
14 | "outputs": [],
15 | "source": [
16 | "import twint\n",
17 | "import nest_asyncio\n",
18 | "import pandas as pd\n",
19 | "import sys, os\n",
20 | "import time\n",
21 | "from tqdm import trange"
22 | ]
23 | },
24 | {
25 | "cell_type": "code",
26 | "execution_count": 2,
27 | "metadata": {},
28 | "outputs": [],
29 | "source": [
30 | "# Solve compatibility issues with notebooks and RunTime errors.\n",
31 | "nest_asyncio.apply()"
32 | ]
33 | },
34 | {
35 | "cell_type": "code",
36 | "execution_count": 3,
37 | "metadata": {},
38 | "outputs": [],
39 | "source": [
40 | "# I was diagnosed with deprssion\n",
41 | "# I am fighting depression \n",
42 | "# I suffer from depression\n",
43 | "DEFAULT_KEYWORD=\"I was diagnosed with depression\"\n",
44 | "DEFAULT_LIMIT=400\n",
45 | "def get_keywords_tweets(keyword=DEFAULT_KEYWORD, limit=DEFAULT_LIMIT):\n",
46 | " c = twint.Config()\n",
47 | " c.Search = keyword\n",
48 | " c.Limit = limit\n",
49 | " c.Store_csv = True\n",
50 | " c.Output = (keyword+\".csv\")\n",
51 | " sys.stdout = open(os.devnull, 'w')\n",
52 | " print(twint.run.Search(c))"
53 | ]
54 | },
55 | {
56 | "cell_type": "code",
57 | "execution_count": 5,
58 | "metadata": {},
59 | "outputs": [],
60 | "source": [
61 | "get_keywords_tweets()"
62 | ]
63 | },
64 | {
65 | "cell_type": "code",
66 | "execution_count": 3,
67 | "metadata": {},
68 | "outputs": [],
69 | "source": [
70 | "key_words = [\"I am diagnosed with depression\",'I am fighting depression','I suffer from depression']\n",
71 | "data = pd.DataFrame() \n",
72 | "for key in key_words:\n",
73 | " data =data.append( pd.read_csv(key+\".csv\")) "
74 | ]
75 | },
76 | {
77 | "cell_type": "code",
78 | "execution_count": 4,
79 | "metadata": {},
80 | "outputs": [
81 | {
82 | "data": {
83 | "text/plain": [
84 | "6000"
85 | ]
86 | },
87 | "execution_count": 4,
88 | "metadata": {},
89 | "output_type": "execute_result"
90 | }
91 | ],
92 | "source": [
93 | "len(data)"
94 | ]
95 | },
96 | {
97 | "cell_type": "code",
98 | "execution_count": 5,
99 | "metadata": {},
100 | "outputs": [],
101 | "source": [
102 | "data = data.drop_duplicates(subset=['user_id'], keep='first') #To drop duplicate usernames "
103 | ]
104 | },
105 | {
106 | "cell_type": "code",
107 | "execution_count": 6,
108 | "metadata": {},
109 | "outputs": [
110 | {
111 | "data": {
112 | "text/plain": [
113 | "2877"
114 | ]
115 | },
116 | "execution_count": 6,
117 | "metadata": {},
118 | "output_type": "execute_result"
119 | }
120 | ],
121 | "source": [
122 | "len(data)"
123 | ]
124 | },
125 | {
126 | "cell_type": "code",
127 | "execution_count": 7,
128 | "metadata": {},
129 | "outputs": [
130 | {
131 | "data": {
132 | "text/plain": [
133 | "('i have been officially diagnosed with clinical depression and anxiety after many years of suffering in silence, not understanding what’s wrong with me. i have reflected my insecurities on my friendships and relationships for the longest time and i am sorry to whoever i hurt.',\n",
134 | " 'slaysiah')"
135 | ]
136 | },
137 | "execution_count": 7,
138 | "metadata": {},
139 | "output_type": "execute_result"
140 | }
141 | ],
142 | "source": [
143 | "data['tweet'].iloc[0],data['username'].iloc[0]"
144 | ]
145 | },
146 | {
147 | "cell_type": "code",
148 | "execution_count": 8,
149 | "metadata": {},
150 | "outputs": [],
151 | "source": [
152 | "user_name=data['username'].iloc[0]\n",
153 | "def get_profile_infos(user_name=user_name):\n",
154 | " c = twint.Config()\n",
155 | " c.Username = user_name\n",
156 | " c.Store_csv = True\n",
157 | " c.Output = (\"Profileinfos.csv\")\n",
158 | " sys.stdout = open(os.devnull, 'w')\n",
159 | " twint.run.Lookup(c)\n"
160 | ]
161 | },
162 | {
163 | "cell_type": "code",
164 | "execution_count": 17,
165 | "metadata": {},
166 | "outputs": [],
167 | "source": [
168 | "get_profile_infos()"
169 | ]
170 | },
171 | {
172 | "cell_type": "code",
173 | "execution_count": null,
174 | "metadata": {},
175 | "outputs": [],
176 | "source": [
177 | "profiles=pd.read_csv(\"Profileinfos.csv\")"
178 | ]
179 | },
180 | {
181 | "cell_type": "code",
182 | "execution_count": null,
183 | "metadata": {},
184 | "outputs": [],
185 | "source": [
186 | "for i in trange(len(data)):\n",
187 | " get_profile_infos(data['username'].iloc[i])"
188 | ]
189 | },
190 | {
191 | "cell_type": "code",
192 | "execution_count": 10,
193 | "metadata": {},
194 | "outputs": [],
195 | "source": [
196 | "user_name=data['username'].iloc[0]\n",
197 | "def get_timeline_by_usernames(user_name=user_name):\n",
198 | " c = twint.Config()\n",
199 | " c.Username =user_name\n",
200 | " c.Retweets = True\n",
201 | " c.Limit=100\n",
202 | " c.Store_csv = True\n",
203 | " c.Output = (\"Timelines/\"+user_name+\".csv\")\n",
204 | " sys.stdout = open(os.devnull, 'w')\n",
205 | " twint.run.Search(c)\n",
206 | " "
207 | ]
208 | },
209 | {
210 | "cell_type": "code",
211 | "execution_count": null,
212 | "metadata": {},
213 | "outputs": [],
214 | "source": [
215 | "for i in trange(len(data)):\n",
216 | " get_timeline_by_usernames(data['username'].iloc[i])"
217 | ]
218 | },
219 | {
220 | "cell_type": "code",
221 | "execution_count": null,
222 | "metadata": {},
223 | "outputs": [],
224 | "source": []
225 | }
226 | ],
227 | "metadata": {
228 | "kernelspec": {
229 | "display_name": "Python 3",
230 | "language": "python",
231 | "name": "python3"
232 | },
233 | "language_info": {
234 | "codemirror_mode": {
235 | "name": "ipython",
236 | "version": 3
237 | },
238 | "file_extension": ".py",
239 | "mimetype": "text/x-python",
240 | "name": "python",
241 | "nbconvert_exporter": "python",
242 | "pygments_lexer": "ipython3",
243 | "version": "3.6.10"
244 | }
245 | },
246 | "nbformat": 4,
247 | "nbformat_minor": 4
248 | }
249 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Depression Detection
3 |
4 |
5 |
6 | Table of Contents
15 |
16 |
17 |
36 |
19 |
22 |
25 |
28 |
43 | Pexels and Unsplash are two freely-usable images platforms.
Tweets used are publicly available.
44 | ### Visual Data:
45 | The overall process of scraping images from unsplash and pexels is presented as follows:
46 |
50 |
53 |
54 | This is a sample of the dataset:
55 |
62 | Overall, 5460 tweets were collected.
63 | The process was:
64 |
74 |
81 |
82 | #### Models for Texts:
83 | Trained two different types of models:
84 |
85 |
88 |
89 |
90 |
91 | For the best models I actually chose, you can find three notebooks:
92 |
93 |
99 |
100 | You can find the saved weights for images best model and texts best model here.
101 |
102 |
103 |
104 | ## Software and technologies: ↑
105 |
106 |
107 |
115 |
116 |
117 |
118 | ## Hardware ↑
119 | In the process of the implementation of our solution we used two main machines,
120 | a local machine for refactoring codes, testing models and research, and a virtual
121 | machine (VM) on Google Cloud Platform (GCP) to run models and codes that
122 | are heavy in term of computation and time. Following are the specifications of
123 | these machines:
124 |
125 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | ---
167 |
168 | ### Finally, I hope you enjoyed my work and got inspired to help people get noticed :monocle_face:. If you want more details you can find my report here .
169 |
170 | Please contact me for more details, I would be really happy to share more infos :yum:.
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
--------------------------------------------------------------------------------
/Notebooks/Bibliography.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Bibliography:"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "## Social Media Use Worldwide:"
15 | ]
16 | },
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {},
20 | "source": [
21 | "#### Introduction:\n",
22 | "The worldwide accessibility to internet is one of the phenomena that reshapes the world as we know it. Social Media is used by nearly 3.5 billion people, which makes it a powerful tool to influence people, change their behaviour and even cause them mental diseases.\n",
23 | "With the Cambridge Analytica scandal, it was shown to the world how personal data can be a tool to understand human behaviour.\n",
24 | "\n",
25 | "As noted in DSM-5 [1], the possibility of suicidal behavior exists at all times during major depressive episodes. According to WHO(World Health Organisation), Due to suicide, 800000 people die every year which makes it the second leading cause of death in 15-29-year-olds. Depression disorder is then considered a serious public health concern with more than 264 million people affected worldwide. \n",
26 | "\n",
27 | "Our study focus essentially on harvesting social networks data to detect early signs of depression and hopefully participate in decreasing the rate of suicide worldwide. \n"
28 | ]
29 | },
30 | {
31 | "cell_type": "markdown",
32 | "metadata": {},
33 | "source": [
34 | " # Articles:\n",
35 | "\n",
36 | "The diagnosis of Depression from Social Networks is effective.[3] [4]\n",
37 | " \n",
38 | " \n",
39 | "The sources of publicly available data used by [2] were:\n",
40 | "\n",
41 | " * Searching public tweets for keywords\n",
42 | " * Public tweets having mental illness keywords such as depression, anxiety, insomnia, suicide...\n",
43 | " * Users sharing their mental health diagnosis\n",
44 | " * Groups/pages of mental health problems\n",
45 | "\n",
46 | "The features were mostly:\n",
47 | " * User activity:\n",
48 | " * number of posts per hour/per day\n",
49 | " * posts between 12-6 AM\n",
50 | " * Likes (types of reaction on facebook can be used)\n",
51 | " * mentions\n",
52 | " * status update (freq)\n",
53 | " * posts content (N-grams)\n",
54 | " * Social networking:\n",
55 | " * Friends (Number)\n",
56 | " * Follows (Pages/groups)\n",
57 | " * Gender/Age/ Education... \n",
58 | " \n",
59 | "We will base our work on the DSM-5 to determine the keywords we will use for Depression, The features extracted from DSM-5 are: stress, depression, suicide, worthless, insomnia, despair, hopeless, lonely, anxiety, struggle\n",
60 | "\n",
61 | "\n",
62 | "\n",
63 | " \n",
64 | "### What is the age range of the population?\n",
65 | "Age will not be fixed due to difficulties to extract it via web-scraping (birth date and Age from Twitter are not always available and can be faulty)\n",
66 | "\n",
67 | "\n",
68 | "\n"
69 | ]
70 | },
71 | {
72 | "cell_type": "markdown",
73 | "metadata": {},
74 | "source": [
75 | " # Datasets:\n",
76 | "### How personality traits is related to depression:\n",
77 | "\n",
78 | "I used a dataset that contains Facebook statuses in raw text and gold standard personality labels (5 big traits) which are Extraversion, Neuroticism, Agreeableness, Conscientiousness and Openness. \n",
79 | "Multiple research (Kotov et al. 2010, Hakulinen et al. 2015) proved that Depression is related to low extraversion, high neuroticism and low conscientiousness.\n",
80 | "\n",
81 | "I Used this to create a binary variable for depression in the dataset.\n",
82 | "\n",
83 | "The My_Personality Dataset was available to the public but was removed due to privacy security.\n",
84 | "\n",
85 | "\n",
86 | "### Another Dataset:\n",
87 | "\n",
88 | "I contacted Sharath Chandra Guntuku the writer of \"What Twitter Profile and Posted Images Reveal About Depression and Anxiety\" and he provided me with a data set containing twitter Ids of people having Depression with an estimated score of depression and anxiety level (the depression score varies from 1.591 to 4.383).\n",
89 | "\n",
90 | "(This is not interested due to the unknown time of the experiment)\n",
91 | "\n",
92 | "### Dataset of labeled status: \n",
93 | "From this article [2] a dataset of labeled status was found publicly available.\n",
94 | "\n"
95 | ]
96 | },
97 | {
98 | "cell_type": "markdown",
99 | "metadata": {},
100 | "source": [
101 | "# Modeling:\n",
102 | "\n",
103 | "The multimodal learning is an interested approach that combines information from different modalities such as images, texts, audio..\n",
104 | "\n",
105 | "This method is challenging due to two reasons: \n",
106 | " * Where the fusion of modalities has to be done?\n",
107 | " * Early Fusion \n",
108 | " * Late Fusion\n",
109 | " * In between\n",
110 | " \n",
111 | " \n",
112 | " * How to combine modalities?\n",
113 | " * Late fusion :\n",
114 | " * Weighted mean\n",
115 | " * Concat: using a single linear perceptron\n",
116 | " * Concat+Multitask: uses the same multi-task-loss of CentralNet (proposed by Vielzeuf et.al)\n",
117 | " * Moddrop \n",
118 | " * Gated Multimodal unit \n",
119 | " * CentralNet: Vielzeuf et.al (Best accuracy on three datasets) \n",
120 | "\n",
121 | "\n",
122 | "There's three github repositories that uses Multimodal learning:\n",
123 | "\n",
124 | " * disaster-response\n",
125 | " * Emotion classifier there's an article related to this repo on medium and a paper article. [5]\n",
126 | " \n",
127 | "## Twitter vs Facebook\n",
128 | "\n",
129 | "There's multiple studies done on detecting Depressive Disorder on Twitter. \n",
130 | "\n",
131 | "For instance: \n",
132 | "\n",
133 | "[6] detected depressive disorders via Twitter.\n",
134 | "In previous studies, online detection was proven effective in Twitter.\n",
135 | "\n",
136 | "Facebook \"protects\" users Data and privacy by those terms .\n",
137 | "\n",
138 | "#Models that will be created:\n",
139 | "##Three architectures will be built:\n",
140 | "* First model: \n",
141 | "\n",
142 | " input: photos and status both labeled. \n",
143 | " \n",
144 | " output: depressed / not depressed\n",
145 | "* Second Model:\n",
146 | "\n",
147 | " input: profile of twitter user (both structured and unstructured data)\n",
148 | " \n",
149 | " output: depressed/ Not depressed\n",
150 | "\n",
151 | "* Third Model:\n",
152 | "\n",
153 | " input: Integrate first and second models into one model accepting two types of unstructured data Images and texts \n",
154 | " \n",
155 | " output: depressed/ Not depressed\n",
156 | "\n"
157 |
158 | ]
159 | },
160 | {
161 | "cell_type": "markdown",
162 | "metadata": {},
163 | "source": [
164 | "References:\n",
165 | "\n",
166 | "\n",
167 | "[1] American Psychiatric Association. Diagnostic and Statistical Manual of Mental Disorders : DSM-5. Arlington, Va., American Psychiatric Association, 2013.\n",
168 | "\n",
169 | "\n",
170 | "[2] Guntuku, Sharath Chandra & Yaden, David & Kern, Margaret & Ungar, Lyle & Eichstaedt, Johannes. (2017). Detecting depression and mental illness on social media: an integrative review. Current Opinion in Behavioral Sciences. 18. 43-49. 10.1016/j.cobeha.2017.07.005. \n",
171 | "\n",
172 | "[3] Shen, Tiancheng & Jia, Jia & Shen, Guangyao & Feng, Fuli & He, Xiangnan & Luan, Huanbo & Tang, Jie & Tiropanis, Thanassis & Chua, Tat-Seng & Hall, Wendy. (2018). Cross-Domain Depression Detection via Harvesting Social Media. 1611-1617. 10.24963/ijcai.2018/223. \n",
173 | "\n",
174 | "[4] Shen, Guangyao & Jia, Jia & Nie, Liqiang & Feng, Fuli & Zhang, Cunjun & Hu, Tianrui & Chua, Tat-Seng & Zhu, Wenwu. (2017). Depression Detection via Harvesting Social Media: A Multimodal Dictionary Learning Solution. 3838-3844. 10.24963/ijcai.2017/536. \n",
175 | "\n",
176 | "[5] Duong, Chi & Lebret, Rémi & Aberer, Karl. (2017). Multimodal Classification for Analysing Social Media. \n",
177 | "\n",
178 | "[6] Gamon, Michael & Choudhury, Munmun & Counts, Scott & Horvitz, Eric. (2013). Predicting Depression via Social Media. Association for the Advancement of Artificial Intelligence. \n"
179 | ]
180 | }
181 | ],
182 | "metadata": {
183 | "kernelspec": {
184 | "display_name": "Python 3",
185 | "language": "python",
186 | "name": "python3"
187 | },
188 | "language_info": {
189 | "codemirror_mode": {
190 | "name": "ipython",
191 | "version": 3
192 | },
193 | "file_extension": ".py",
194 | "mimetype": "text/x-python",
195 | "name": "python",
196 | "nbconvert_exporter": "python",
197 | "pygments_lexer": "ipython3",
198 | "version": "3.6.10"
199 | }
200 | },
201 | "nbformat": 4,
202 | "nbformat_minor": 4
203 | }
204 |
--------------------------------------------------------------------------------
/Notebooks/TwitterscraperDemo.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Twitter scraper"
8 | ]
9 | },
10 | {
11 | "cell_type": "code",
12 | "execution_count": 35,
13 | "metadata": {},
14 | "outputs": [],
15 | "source": [
16 | "from twitterscraper import query_tweets,query_user_info,query_tweets_from_user"
17 | ]
18 | },
19 | {
20 | "cell_type": "code",
21 | "execution_count": 36,
22 | "metadata": {},
23 | "outputs": [
24 | {
25 | "name": "stderr",
26 | "output_type": "stream",
27 | "text": [
28 | "INFO: Using proxy 47.75.11.94:8080\n",
29 | "INFO: Got user information from username @Alwaleed_Talal\n",
30 | "INFO: Using proxy 200.89.178.228:3128\n",
31 | "INFO: Got user information from username @Moncef_Marzouki\n"
32 | ]
33 | }
34 | ],
35 | "source": [
36 | "info=query_user_info(user='@Alwaleed_Talal') \n",
37 | "info\n",
38 | "info=query_user_info(user='@Moncef_Marzouki')\n",
39 | "info\n",
40 | "## these are two users that theirs profiles can't be scraped by twitter scraper "
41 | ]
42 | },
43 | {
44 | "cell_type": "code",
45 | "execution_count": 37,
46 | "metadata": {},
47 | "outputs": [
48 | {
49 | "name": "stderr",
50 | "output_type": "stream",
51 | "text": [
52 | "INFO: Using proxy 203.162.21.216:8000\n",
53 | "INFO: Got user information from username @rebaisaber\n"
54 | ]
55 | },
56 | {
57 | "data": {
58 | "text/plain": [
59 | "3756880"
60 | ]
61 | },
62 | "execution_count": 37,
63 | "metadata": {},
64 | "output_type": "execute_result"
65 | }
66 | ],
67 | "source": [
68 | "info=query_user_info(user='@rebaisaber')\n",
69 | "# even though the name is in arabic it scrapes its content\n",
70 | "info.followers\n"
71 | ]
72 | },
73 | {
74 | "cell_type": "code",
75 | "execution_count": 38,
76 | "metadata": {},
77 | "outputs": [
78 | {
79 | "name": "stderr",
80 | "output_type": "stream",
81 | "text": [
82 | "INFO: Using proxy 165.227.215.71:8080\n",
83 | "INFO: Got user information from username @namatahara\n",
84 | "INFO: Using proxy 62.118.131.200:3127\n",
85 | "INFO: Got user information from username @anatoliisharii\n"
86 | ]
87 | }
88 | ],
89 | "source": [
90 | "infoChinese=query_user_info(user='@namatahara')\n",
91 | "infoRuss=query_user_info(user='@anatoliisharii')\n",
92 | "# The language is not the problem "
93 | ]
94 | },
95 | {
96 | "cell_type": "code",
97 | "execution_count": 39,
98 | "metadata": {},
99 | "outputs": [
100 | {
101 | "name": "stderr",
102 | "output_type": "stream",
103 | "text": [
104 | "INFO: Scraping tweets from https://twitter.com/@ityaadie\n",
105 | "INFO: Using proxy 114.134.190.230:37294\n",
106 | "INFO: Got 20 tweets from username @ityaadie\n"
107 | ]
108 | }
109 | ],
110 | "source": [
111 | "Q=query_tweets_from_user('@ityaadie',limit=3)\n",
112 | "#per page : query_single_page"
113 | ]
114 | },
115 | {
116 | "cell_type": "code",
117 | "execution_count": 40,
118 | "metadata": {},
119 | "outputs": [
120 | {
121 | "data": {
122 | "text/plain": [
123 | "list"
124 | ]
125 | },
126 | "execution_count": 40,
127 | "metadata": {},
128 | "output_type": "execute_result"
129 | }
130 | ],
131 | "source": [
132 | "type(Q)"
133 | ]
134 | },
135 | {
136 | "cell_type": "code",
137 | "execution_count": 41,
138 | "metadata": {},
139 | "outputs": [
140 | {
141 | "name": "stdout",
142 | "output_type": "stream",
143 | "text": [
144 | "0\n",
145 | "bns tummy is my new comfy spot here see a selfypic.twitter.com/1EJms2rQFb\n",
146 | "1\n",
147 | "Sex is great (I’m assuming here) but have y’all ever been hugged like you’re the most precious thing in their arms? As if they’d lose something if they let you go? So tight that you felt yourself feel whole?\n",
148 | "\n",
149 | "If it’s not obvious, I want hugs\n",
150 | "2\n",
151 | "This is the cutest thing I've seen in a while!https://twitter.com/shrishrishrii/status/1230359993683042304 …\n",
152 | "3\n",
153 | "Time Circles\n",
154 | "41mins:44seconds in the life of circles\n",
155 | "\n",
156 | "creativecoding #processing #basiljs #indesign #javascript #p5js\n",
157 | "#programmingfordesigners #generativedesign #generativeart @ UIC School of Design https://www.instagram.com/p/B8xWqbFJDKN/?igshid=n7vjrrp8rspq …\n",
158 | "4\n",
159 | "I hate that part of asian culture where the men eat before the women. Everyone should jus eat at the same damn time on the table\n",
160 | "5\n",
161 | "and more basil.js\n",
162 | "...\n",
163 | "\n",
164 | " #creativecoding #processing #basiljs #indesign #javascript\n",
165 | "#programmingfordesigners @ UIC School of Design https://www.instagram.com/p/B8iOULMJSvu/?igshid=11td02o8obj5p …\n",
166 | "6\n",
167 | "more experiments with basil.js\n",
168 | "...\n",
169 | "\n",
170 | " #creativecoding #processing #basiljs #indesign #javascript\n",
171 | "#programmingfordesigners #p5js @ UIC School of Design https://www.instagram.com/p/B8iODEhpz9M/?igshid=zp5ewwfj9c76 …\n",
172 | "7\n",
173 | "Initial expeiments with basil.js during a workshop with teddavisdotorg \n",
174 | "\n",
175 | "...\n",
176 | "\n",
177 | "#creativecoding #processing #basiljs #indesign #javascript\n",
178 | "#programmingfordesigners @ UIC School of Design https://www.instagram.com/p/B8iNqbWpj2P/?igshid=1eeqiw9mr7bnu …\n",
179 | "8\n",
180 | "28 days in a row https://www.headspace.com/sharing-is-caring/Coo9reCLSGOqkt-AruSpUw …pic.twitter.com/r7fDArf2BJ\n",
181 | "9\n",
182 | "drinkiespic.twitter.com/MPTJ2IsMRE\n",
183 | "10\n",
184 | "THERE IS NOT ENOUGH TIME IN A DAY TO GO TO CLASS AND STUDY HARD AND GO TO WORK AND MAKE HEALTHY MEALS AND GO TO THE GYM AND SPEND TIME WITH FRIENDS AND FAMILY AND I’M SICK OF IT\n",
185 | "11\n",
186 | "\"What You Missed that day you were absent from Fourth Grade\" by Brad Aaron Modlinpic.twitter.com/luX2EowPAZ\n",
187 | "12\n",
188 | "Me vs who I share my birthday with.pic.twitter.com/CNmOQr65t1\n",
189 | "13\n",
190 | "#itMepic.twitter.com/ZhKzBQecoR\n",
191 | "14\n",
192 | "Every day is a good day to meditate. https://www.headspace.com/sharing-is-caring/bdPUABndSN6vrsJY_wQqww …pic.twitter.com/IDNaJiYwjR\n",
193 | "15\n",
194 | "At this point, I'd marry just anybody but I cannot, will not settle for just any therapist.\n",
195 | "16\n",
196 | "pic.twitter.com/htX6w9mJDG\n",
197 | "17\n",
198 | "Mumbai still has internet, permission to protest, and a non-BJP government. Read up on your rights, take your friends, convince your co-workers, and show up at August Kranti Maidan at 4 pm today. #IndiaAgainstCAA\n",
199 | "18\n",
200 | "My parents this morning: If you get detained, we're not coming to get you. \n",
201 | "\n",
202 | "I'm so glad cowardice and apathy aren't hereditary.\n",
203 | "19\n",
204 | "\"meet me in the narrow neck of an hourglass\n",
205 | "where I can't find the time to see where I you end and I begin\"\n",
206 | "\n",
207 | "\n"
208 | ]
209 | }
210 | ],
211 | "source": [
212 | "for i in range(len(Q)):\n",
213 | " print(i)\n",
214 | " print(Q[i].text)"
215 | ]
216 | },
217 | {
218 | "cell_type": "code",
219 | "execution_count": 48,
220 | "metadata": {},
221 | "outputs": [
222 | {
223 | "data": {
224 | "text/plain": [
225 | "{'screen_name': 'ityaadie',\n",
226 | " 'username': 'depression cherry',\n",
227 | " 'user_id': '310830503',\n",
228 | " 'tweet_id': '897330478012813312',\n",
229 | " 'tweet_url': '/ityaadie/status/897330478012813312',\n",
230 | " 'timestamp': datetime.datetime(2017, 8, 15, 5, 33, 52),\n",
231 | " 'timestamp_epochs': 1502775232,\n",
232 | " 'text': 'bns tummy is my new comfy spot here see a selfypic.twitter.com/1EJms2rQFb',\n",
233 | " 'text_html': '
127 |
132 |
I used two configurations:
137 |
138 |
145 |
148 |
154 |
bns tummy is my new comfy spot here see a selfypic.twitter.com/1EJms2rQFb
',\n", 234 | " 'links': [],\n", 235 | " 'hashtags': [],\n", 236 | " 'has_media': True,\n", 237 | " 'img_urls': ['https://pbs.twimg.com/media/DHP1R28UIAEjjRi.jpg'],\n", 238 | " 'video_url': '',\n", 239 | " 'likes': 151,\n", 240 | " 'retweets': 10,\n", 241 | " 'replies': 10,\n", 242 | " 'is_replied': True,\n", 243 | " 'is_reply_to': False,\n", 244 | " 'parent_tweet_id': '',\n", 245 | " 'reply_to_users': []}" 246 | ] 247 | }, 248 | "execution_count": 48, 249 | "metadata": {}, 250 | "output_type": "execute_result" 251 | } 252 | ], 253 | "source": [ 254 | "Q[0].__dict__" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": 49, 260 | "metadata": {}, 261 | "outputs": [ 262 | { 263 | "data": { 264 | "text/plain": [ 265 | "'310830503'" 266 | ] 267 | }, 268 | "execution_count": 49, 269 | "metadata": {}, 270 | "output_type": "execute_result" 271 | } 272 | ], 273 | "source": [ 274 | "Q[0].user_id" 275 | ] 276 | }, 277 | { 278 | "cell_type": "markdown", 279 | "metadata": {}, 280 | "source": [ 281 | "# Twitter_scraper\n", 282 | "\n", 283 | "For personal information extraction of users we can use this." 284 | ] 285 | }, 286 | { 287 | "cell_type": "code", 288 | "execution_count": 9, 289 | "metadata": {}, 290 | "outputs": [], 291 | "source": [ 292 | "from twitter_scraper import Profile" 293 | ] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": 44, 298 | "metadata": {}, 299 | "outputs": [ 300 | { 301 | "name": "stdout", 302 | "output_type": "stream", 303 | "text": [ 304 | "Name: محمد المنصف المرزوقي \n", 305 | " Username: @Moncef_Marzouki \n", 306 | " followers: 502720 \n", 307 | " Birthday: None \n", 308 | " Website: moncefmarzouki.net \n", 309 | " link to profile photo : https://pbs.twimg.com/profile_images/1171090653218054146/prECMbzH_400x400.jpg \n", 310 | " Likes: None \n", 311 | " Tweets count : 2409 \n", 312 | " following: 12\n" 313 | ] 314 | } 315 | ], 316 | "source": [ 317 | "profile = Profile(\"@Moncef_Marzouki\")\n", 318 | "print(\"Name:\",profile.name,\n", 319 | " \"\\n Username:\",profile.username,\n", 320 | " \"\\n followers:\",profile.followers_count,\n", 321 | " \"\\n Birthday:\",profile.birthday,\n", 322 | " \"\\n Website:\", profile.website,\n", 323 | " \"\\n link to profile photo :\",profile.profile_photo,\n", 324 | " \"\\n Likes:\",profile.likes_count,\n", 325 | " \"\\n Tweets count :\",profile.tweets_count,\n", 326 | " \"\\n following:\",profile.following_count)" 327 | ] 328 | } 329 | ], 330 | "metadata": { 331 | "kernelspec": { 332 | "display_name": "Python 3", 333 | "language": "python", 334 | "name": "python3" 335 | }, 336 | "language_info": { 337 | "codemirror_mode": { 338 | "name": "ipython", 339 | "version": 3 340 | }, 341 | "file_extension": ".py", 342 | "mimetype": "text/x-python", 343 | "name": "python", 344 | "nbconvert_exporter": "python", 345 | "pygments_lexer": "ipython3", 346 | "version": "3.6.10" 347 | } 348 | }, 349 | "nbformat": 4, 350 | "nbformat_minor": 4 351 | } 352 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /ResNet-Transfer.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Load Data " 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "from data import Images_load" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 2, 22 | "metadata": {}, 23 | "outputs": [ 24 | { 25 | "name": "stdout", 26 | "output_type": "stream", 27 | "text": [ 28 | "Data has apparently already been downloaded and unpacked.\n", 29 | "Data has apparently already been downloaded and unpacked.\n" 30 | ] 31 | }, 32 | { 33 | "name": "stderr", 34 | "output_type": "stream", 35 | "text": [ 36 | "100%|██████████| 4006/4006 [00:09<00:00, 442.29it/s]\n", 37 | "100%|██████████| 4192/4192 [00:10<00:00, 386.32it/s]\n" 38 | ] 39 | } 40 | ], 41 | "source": [ 42 | "train, validation, test = Images_load.load_data()" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 3, 48 | "metadata": {}, 49 | "outputs": [ 50 | { 51 | "data": { 52 | "text/plain": [ 53 | "(4611, 224, 224, 3)" 54 | ] 55 | }, 56 | "execution_count": 3, 57 | "metadata": {}, 58 | "output_type": "execute_result" 59 | } 60 | ], 61 | "source": [ 62 | "train.features.shape" 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "metadata": {}, 68 | "source": [ 69 | "# ResNet 50 transfer learning: " 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 7, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "import keras\n", 79 | "from keras.models import Sequential\n", 80 | "from keras.layers import Dense, Flatten\n", 81 | "from keras.applications.resnet50 import ResNet50, decode_predictions, preprocess_input\n" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 9, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "def _prepare_data(train, validation, test):\n", 91 | " \"\"\"\n", 92 | " Prepare datasets of images for CNN\n", 93 | " :param train:\n", 94 | " :param validation:\n", 95 | " :param test:\n", 96 | " :return:\n", 97 | " \"\"\"\n", 98 | " features, y_train = train.features, train.labels\n", 99 | " featuresV, y_val = validation.features, validation.labels\n", 100 | " featuresT, y_test = test.features, test.labels\n", 101 | " x_train = np.stack(features)\n", 102 | " x_val = np.stack(featuresV)\n", 103 | " x_test = np.stack(featuresT)\n", 104 | " x_train = x_train.astype('float32')\n", 105 | " x_test = x_test.astype('float32')\n", 106 | " x_val = x_val.astype('float32')\n", 107 | " x_val /= 255\n", 108 | " x_train /= 255\n", 109 | " x_test /= 255\n", 110 | "\n", 111 | " return x_train, y_train, x_test, y_test, x_val, y_val" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 11, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "import numpy as np\n", 121 | "x_train, y_train, x_test, y_test, x_val, y_val=_prepare_data(train,validation,test)" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": 12, 127 | "metadata": {}, 128 | "outputs": [ 129 | { 130 | "name": "stdout", 131 | "output_type": "stream", 132 | "text": [ 133 | "WARNING:tensorflow:From /opt/conda/lib/python3.7/site-packages/tensorflow_core/python/ops/resource_variable_ops.py:1630: calling BaseResourceVariable.__init__ (from tensorflow.python.ops.resource_variable_ops) with constraint is deprecated and will be removed in a future version.\n", 134 | "Instructions for updating:\n", 135 | "If using Keras pass *_constraint arguments to layers.\n", 136 | "WARNING:tensorflow:From /opt/conda/lib/python3.7/site-packages/keras/backend/tensorflow_backend.py:4070: The name tf.nn.max_pool is deprecated. Please use tf.nn.max_pool2d instead.\n", 137 | "\n", 138 | "Downloading data from https://github.com/fchollet/deep-learning-models/releases/download/v0.2/resnet50_weights_tf_dim_ordering_tf_kernels.h5\n", 139 | "102858752/102853048 [==============================] - 9s 0us/step\n" 140 | ] 141 | } 142 | ], 143 | "source": [ 144 | "# Load ResNet50 Trained on imagenet\n", 145 | "resnet_model = ResNet50(weights=\"imagenet\")\n" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": 20, 151 | "metadata": {}, 152 | "outputs": [], 153 | "source": [ 154 | "# We should preprocess the images the same way resnet images were preprocessed\n", 155 | "x_train_preprocessed = preprocess_input(x_train)\n", 156 | "x_test_preprocessed = preprocess_input(x_test)\n", 157 | "x_val_preprocess = preprocess_input(x_val)" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": 16, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "# Build a new model that is ResNet50 minus the very last layer\n", 167 | "last_layer = resnet_model.get_layer(\"avg_pool\")\n", 168 | "\n", 169 | "resnet_layers = keras.Model(inputs=resnet_model.inputs, outputs=last_layer.output)" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 30, 175 | "metadata": {}, 176 | "outputs": [ 177 | { 178 | "name": "stdout", 179 | "output_type": "stream", 180 | "text": [ 181 | "Model: \"sequential_4\"\n", 182 | "_________________________________________________________________\n", 183 | "Layer (type) Output Shape Param # \n", 184 | "=================================================================\n", 185 | "model_2 (Model) (None, 2048) 23587712 \n", 186 | "_________________________________________________________________\n", 187 | "dense_4 (Dense) (None, 2) 4098 \n", 188 | "=================================================================\n", 189 | "Total params: 23,591,810\n", 190 | "Trainable params: 4,098\n", 191 | "Non-trainable params: 23,587,712\n", 192 | "_________________________________________________________________\n" 193 | ] 194 | } 195 | ], 196 | "source": [ 197 | "# We can directly stich the models together\n", 198 | "\n", 199 | "ResNet_adapt=Sequential()\n", 200 | "ResNet_adapt.add(resnet_layers)\n", 201 | "ResNet_adapt.add(Dense(2, activation=\"sigmoid\"))\n", 202 | "\n", 203 | "ResNet_adapt.layers[0].trainable=False # we are just going to tune the last layer weight the other weights inside of the resnet model will remain the same \n", 204 | "\n", 205 | "ResNet_adapt.compile(loss=\"binary_crossentropy\", optimizer=\"adam\", metrics=[\"accuracy\"])\n", 206 | "\n", 207 | "ResNet_adapt.summary()" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 31, 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [ 216 | "from keras.callbacks import EarlyStopping, ModelCheckpoint\n", 217 | "from keras.callbacks import TensorBoard\n", 218 | "my_callbacks = [\n", 219 | " EarlyStopping(monitor='val_loss', patience=10, verbose=1, mode='auto', restore_best_weights=True),\n", 220 | " ModelCheckpoint(filepath='Resnet_transfer.{epoch:02d}-{val_loss:.2f}.h5',\n", 221 | " monitor='val_accuracy', verbose=1,\n", 222 | " save_best_only=True, mode='max'),\n", 223 | " TensorBoard(log_dir=\"logs\", histogram_freq=0, write_graph=True, write_images=True)\n", 224 | " ]" 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": 32, 230 | "metadata": {}, 231 | "outputs": [ 232 | { 233 | "name": "stdout", 234 | "output_type": "stream", 235 | "text": [ 236 | "Train on 4611 samples, validate on 1538 samples\n", 237 | "Epoch 1/50\n", 238 | "4611/4611 [==============================] - 751s 163ms/step - loss: 0.5920 - accuracy: 0.6852 - val_loss: 1.1036 - val_accuracy: 0.4844\n", 239 | "\n", 240 | "Epoch 00001: val_accuracy improved from -inf to 0.48440, saving model to Resnet_transfer.01-1.10.h5\n", 241 | "Epoch 2/50\n", 242 | "4611/4611 [==============================] - 733s 159ms/step - loss: 0.5507 - accuracy: 0.7224 - val_loss: 1.1658 - val_accuracy: 0.4844\n", 243 | "\n", 244 | "Epoch 00002: val_accuracy did not improve from 0.48440\n", 245 | "Epoch 3/50\n", 246 | "4611/4611 [==============================] - 754s 164ms/step - loss: 0.5292 - accuracy: 0.7427 - val_loss: 1.2232 - val_accuracy: 0.4844\n", 247 | "Restoring model weights from the end of the best epoch\n", 248 | "\n", 249 | "Epoch 00003: val_accuracy did not improve from 0.48440\n", 250 | "Epoch 00003: early stopping\n", 251 | "CPU times: user 3h 11min 16s, sys: 1h 45min 28s, total: 4h 56min 45s\n", 252 | "Wall time: 37min 30s\n" 253 | ] 254 | } 255 | ], 256 | "source": [ 257 | "%%time \n", 258 | "history=ResNet_adapt.fit(x_train_preprocessed, y_train, epochs=50, validation_data=(x_val_preprocess, y_val), callbacks=my_callbacks)" 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": 33, 264 | "metadata": {}, 265 | "outputs": [ 266 | { 267 | "data": { 268 | "image/png": "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\n", 269 | "text/plain": [ 270 | "