├── .gitignore
├── LICENSE
├── README.md
├── dataset_builder_multi_label.py
├── find_best_model.py
├── geoguessr_dataset.py
├── get_images.py
├── main.py
├── notebook
└── GeoGuessr_AI_Demo.ipynb
├── save_production_model.py
└── utils
└── tensor_utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | # Machine Learning
132 | /models
133 | /images
134 | /geoguessr_dataset
135 | /runs
136 | /tensorboard
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Alex
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GeoGuessr AI
2 |
3 | [](https://colab.research.google.com/github/Stelath/geoguessr-ai/blob/main/notebook/GeoGuessr_AI_Demo.ipynb) 
4 |
5 | This project was done personally as an opportunity to learn more about CNN & MLPs by creating a ML Model that could reliably guess a random location in one of five US cities given only a Google Street View image. The idea was inspired by the game GeoGuessr where the user is given a random Google Street View location and have to guess based on the Street View their location in the world.
6 |
7 | ## Update
8 | Stanford Grad Students have recently released a model called PIGEON which significantly outperforms ours, I suggest you check it out [here](https://huggingface.co/geolocal/StreetCLIP)
9 |
10 | ## Project Overview
11 |
12 | ### Creating a Dataset
13 |
14 | In order to train the model I first had to create a reasonably large dataset of Google Street View images to train it on. To do this I wrote a python script ([get_images.py](https://github.com/Stelath/geoguessr-ai/blob/main/get_images.py "get_images.py")) to download a large set of photographs from 5 cities in the US from Google Street View API. In order to download images from the Google Street View API latitude and longitude coordinates were needed, to solve this I utilized an address book from [Open Addresses](https://openaddresses.io/) to get the latitude and longitude data of random street addresses for each of the 5 cities.
15 |
16 | ### Formatting the Images
17 |
18 | Before training the model I decided to format the targets in order to create a way for the AI to better guess the location by turning the GPS coordinates into multi class targets, through formatting each number in the coordinate as a one hot array with numbers ranging one through ten.
19 |
20 | ### Training the Model
21 |
22 | After using a couple different model architectures I settled on a wideresnet with 50 layers which gave comparable results to a wideresnet with 100 layers but took far less GPU memory. The model was trained on a dataset of 50,000 images and had the best performance 20 epochs in and then began to slowly overfit.
23 |
24 | ### How Could it be Improved?
25 |
26 | Using a custom model that is better suited to guessing locations rather than just image classification. Adding far more layers so the model can pick up more complexity however this would require a larger GPU. Arguably the best performance improver - and something that would more accurately follow the GeoGuessr - idea would be to train a 3D CNN on an array of images from the location giving the model far more data to work with and make accurate predictions.
27 |
28 | ### Takeaway
29 |
30 | While the model is by no means perfect, it is suprisingly accurate given the limited input it recives. This interestingly enough reveals that while many would regard American cities as similar there are clearly significant differences in their landscapes so much so that the AI was able to take advantage of them to at least correctly predict the city it was in the majority of hte time.
31 |
32 | ## Instructions
33 |
34 | ### Run Pretrained Model
35 |
36 | In order to run a pretrained model you can download it from Google Drive: [](https://drive.google.com/file/d/1VJpeLJp6jC8IUfKy6cAtZ9WZcX1TTutW/view?usp=sharing) or you can use the Google Colab: [](https://colab.research.google.com/github/Stelath/geoguessr-ai/blob/main/notebook/GeoGuessr_AI_Demo.ipynb).
37 |
38 | ### Train a Model
39 | You can use the [get_images.py](https://github.com/Stelath/geoguessr-ai/blob/main/get_images.py "get_images.py") script to download a database of images through Google Cloud, they allow 28,500 free Google Street View API calls each month so keep that in mind (anything more and you will be charged $0.007 per image), you will also have to set up a Google Cloud account. You can also download the database of Google Street View images I created [here](https://www.kaggle.com/stelath/city-street-view-dataset).
40 |
41 | After you have a database of images running the [dataset_builder_multi_label.py](https://github.com/Stelath/geoguessr-ai/blob/main/dataset_builder_multi_label.py) script will preprocess all of the images, then running [main.py](https://github.com/Stelath/geoguessr-ai/blob/main/main.py) will begin training the model.
42 |
43 | Here is a set of commands that would be used to train a model on 25,000 images (keep in mind you will need a cities folder containing `.geojson` files from [Open Addresses](https://openaddresses.io/)):
44 | ```
45 | python -m get_images --cities cities/ --output images/ --icount 25000 --key (YOUR GSV API KEY HERE)
46 | python -m dataset_builder_multi_label --file images/picture_coords.csv --images images/ --output geoguessr_dataset/
47 | python -m main geoguessr_dataset/ -a wide_resnet50_2 -b 16 --lr 0.0001 -j 6 --checkpoint-step 1
48 | ```
49 |
--------------------------------------------------------------------------------
/dataset_builder_multi_label.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import csv
3 | from PIL import Image
4 | from tqdm import tqdm
5 | from random import randint
6 | import numpy as np
7 | import os
8 |
9 | def get_args():
10 | parser = argparse.ArgumentParser()
11 | parser.add_argument("--file", help="The CSV file to read and extract GPS coordinates from", required=True, type=str)
12 | parser.add_argument("--images", help="The path to the images folder, (defaults to: images/)", default='images/', type=str)
13 | parser.add_argument("--output", help="The output folder", required=True, type=str)
14 | return parser.parse_args()
15 |
16 | args = get_args()
17 |
18 | targets_train = []
19 | targets_val = []
20 |
21 | def multi_label(num, decimals=4):
22 | num_original = str(abs(float(num)))
23 |
24 | if decimals > 0:
25 | num = str(round(float(num) * (10 ** decimals)))
26 | else:
27 | num = str(round(float(num)))
28 |
29 | if num[0] == '-':
30 | label = np.array([1])
31 | num = num[1:]
32 | else:
33 | label = np.array([0])
34 |
35 | num = num.ljust(len(num_original.split('.')[0]) + decimals, '0')
36 | num = num.zfill(decimals + 3)
37 |
38 | for digit in num:
39 | label = np.concatenate((label, np.eye(10)[int(digit)]))
40 |
41 | return label
42 |
43 | def get_data(coord, coord_index):
44 | lat, lon = coord[0], coord[1]
45 |
46 | img_path = os.path.join(args.images, f'street_view_{coord_index}.jpg')
47 | img = Image.open(img_path)
48 |
49 | lat_multi_label = multi_label(lat)
50 | lon_multi_label = multi_label(lon)
51 |
52 | target = np.concatenate((lat_multi_label, lon_multi_label))
53 |
54 | return [img, target]
55 |
56 | def main():
57 | with open(args.file, 'r') as f:
58 | coords_reader = csv.reader(f)
59 | coords = []
60 | for row in coords_reader:
61 | coords.append(row)
62 |
63 |
64 | train_data_path = os.path.join(args.output, 'train')
65 | os.makedirs(train_data_path, exist_ok=True)
66 | val_data_path = os.path.join(args.output, 'val')
67 | os.makedirs(val_data_path, exist_ok=True)
68 |
69 | val_count = 0
70 | train_count = 0
71 |
72 | for coord_index, coord in enumerate(tqdm(coords)):
73 | if randint(0, 9) == 0:
74 | data = get_data(coord, coord_index)
75 | val_data_path = os.path.join(args.output, f'val/street_view_{val_count}.jpg')
76 | data[0].save(val_data_path)
77 | targets_val.append(data[1])
78 | val_count += 1
79 | else:
80 | data = get_data(coord, coord_index)
81 | train_data_path = os.path.join(args.output, f'train/street_view_{train_count}.jpg')
82 | data[0].save(train_data_path)
83 | targets_train.append(data[1])
84 | train_count += 1
85 |
86 | np.save(os.path.join(args.output, f'train/targets.npy'), np.array(targets_train))
87 | np.save(os.path.join(args.output, f'val/targets.npy'), np.array(targets_val))
88 |
89 | print('Train Files:', train_count)
90 | print('Val Files:', val_count)
91 |
92 | if __name__ == '__main__':
93 | main()
94 |
--------------------------------------------------------------------------------
/find_best_model.py:
--------------------------------------------------------------------------------
1 | import os
2 | import argparse
3 | from tqdm import tqdm
4 | import numpy as np
5 | from datetime import datetime
6 |
7 | import torch
8 | import torch.nn as nn
9 | import torch.utils.data
10 | import torchvision.models as models
11 | from geoguessr_dataset import GeoGuessrDataset
12 |
13 | model_names = sorted(name for name in models.__dict__
14 | if name.islower() and not name.startswith("__")
15 | and callable(models.__dict__[name]))
16 |
17 | parser = argparse.ArgumentParser(description='PyTorch GeoGuessr AI Best Model Locator')
18 | parser.add_argument('data', metavar='DIR',
19 | help='path to dataset')
20 | parser.add_argument('-a', '--arch', metavar='ARCH', default='resnet50',
21 | choices=model_names,
22 | help='model architecture: ' +
23 | ' | '.join(model_names) +
24 | ' (default: resnet50)')
25 | parser.add_argument('-j', '--workers', default=4, type=int, metavar='N',
26 | help='number of data loading workers (default: 4)')
27 | parser.add_argument('-b', '--batch-size', default=64, type=int,
28 | metavar='N',
29 | help='batch size (default: 64), this is the total '
30 | 'batch size of the GPU')
31 | parser.add_argument('--models-dir', default='models', type=str)
32 |
33 | start_time = datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
34 | args = parser.parse_args()
35 | all_loss = []
36 |
37 | def fwd_pass(model, data, targets, loss_function, optimizer, train=False):
38 | data = data.cuda()
39 | targets = targets.cuda()
40 |
41 | if train:
42 | model.zero_grad()
43 |
44 | outputs = model(data)
45 | matches = [(torch.where(i >= 0.5, 1, 0) == j).all() for i, j in zip(outputs, targets)]
46 | acc = matches.count(True) / len(matches)
47 | loss = loss_function(outputs, targets)
48 |
49 | if train:
50 | loss.backward()
51 | optimizer.step()
52 |
53 | return acc, loss
54 |
55 | def test(val_loader, model, loss_function):
56 | model.eval()
57 | acc = []
58 | loss = []
59 |
60 | for idx, sample in enumerate(tqdm(val_loader)):
61 | if idx < 256:
62 | data, target = sample
63 | batch_acc, batch_loss = fwd_pass(model, data, target, loss_function, None)
64 | acc.append(batch_acc)
65 | loss.append(batch_loss.cpu().detach().numpy())
66 |
67 | acc = np.mean(acc)
68 | loss = np.mean(loss)
69 |
70 | val_acc = np.mean(acc)
71 | val_loss = np.mean(loss)
72 | return val_acc, val_loss
73 |
74 | def main():
75 | torch.device("cuda")
76 | valdir = os.path.join(args.data, 'val')
77 | val_dataset = GeoGuessrDataset(valdir)
78 |
79 | val_loader = torch.utils.data.DataLoader(
80 | val_dataset, batch_size=args.batch_size, shuffle=False,
81 | num_workers=args.workers, pin_memory=True)
82 |
83 | print("=> creating model '{}'".format(args.arch))
84 | model = models.__dict__[args.arch](pretrained=False, progress=True, num_classes=142)
85 | model = nn.Sequential(
86 | model,
87 | nn.Sigmoid()
88 | )
89 | model.cuda()
90 |
91 | loss_function = nn.BCELoss()
92 |
93 | for model_path in tqdm(os.listdir(args.models_dir)):
94 | model_path = os.path.join(args.models_dir, model_path)
95 | print("=> loading model '{}'".format(model_path))
96 | checkpoint = torch.load(model_path)
97 | model.load_state_dict(checkpoint['model_state_dict'])
98 | print("=> loaded model '{}' (epoch {})".format(model_path, checkpoint['epoch']))
99 |
100 | val_acc, val_loss = test(val_loader, model, loss_function)
101 | all_loss.append(val_loss)
102 | print("=> val_acc: {:.4f}, val_loss: {:.4f}".format(val_acc, val_loss))
103 |
104 | min_value = min(all_loss)
105 | min_index = all_loss.index(min_value)
106 | print("=> best model: {}, loss: {}".format(os.listdir(args.models_dir)[min_index], min_value))
107 |
108 | if __name__ == '__main__':
109 | main()
110 |
--------------------------------------------------------------------------------
/geoguessr_dataset.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from torch.utils.data import Dataset
3 | import torchvision.transforms as transforms
4 | import numpy as np
5 | import os, os.path
6 | from PIL import Image
7 |
8 | class GeoGuessrDataset(Dataset):
9 | def __init__(self, data_dir):
10 | self.data_dir = data_dir
11 | self.targets = np.load(os.path.join(data_dir, 'targets.npy'), allow_pickle=True)
12 |
13 | def __len__(self):
14 | return len(os.listdir(self.data_dir)) - 1
15 |
16 | def __getitem__(self, idx):
17 | data_path = os.path.join(self.data_dir, f'street_view_{idx}.jpg')
18 |
19 | normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406],
20 | std=[0.229, 0.224, 0.225])
21 | transform = transforms.Compose([
22 | transforms.Resize(256),
23 | transforms.ToTensor(),
24 | normalize,
25 | ])
26 |
27 | img = pil_loader(data_path)
28 | data = transform(img)
29 |
30 | target = torch.tensor(self.targets[idx], dtype=torch.float)
31 |
32 | return data, target
33 |
34 | def pil_loader(path: str) -> Image.Image:
35 | with open(path, "rb") as f:
36 | img = Image.open(f)
37 | return img.convert("RGB")
--------------------------------------------------------------------------------
/get_images.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from tqdm import tqdm
3 | import os
4 | import json
5 | from random import randint
6 | import argparse
7 | from csv import writer
8 | # Consider using https://osmnx.readthedocs.io/en/stable/osmnx.html#osmnx.utils_geo.sample_points for street gps coords
9 | # Stack Overflow on how to get the coordinates: https://stackoverflow.com/questions/68367074/how-to-generate-random-lat-long-points-within-geographical-boundaries
10 |
11 | def get_args():
12 | parser = argparse.ArgumentParser()
13 | parser.add_argument("--cities", help="The folder full of addresses per city to read and extract GPS coordinates from", required=True, type=str)
14 | parser.add_argument("--output", help="The output folder where the images will be stored, (defaults to: images/)", default='images/', type=str)
15 | parser.add_argument("--icount", help="The amount of images to pull (defaults to 25,000)", default=25000, type=int)
16 | parser.add_argument("--key", help="Your Google Street View API Key", type=str, required=True)
17 | return parser.parse_args()
18 |
19 | args = get_args()
20 | url = 'https://maps.googleapis.com/maps/api/streetview'
21 | cities = []
22 |
23 | def load_cities():
24 | for city in os.listdir(args.cities):
25 | with open(os.path.join(args.cities, city)) as f:
26 | coordinates = []
27 | print(f'Loading {city} addresses...')
28 | for line in tqdm(f):
29 | data = json.loads(line)
30 | coordinates.append(data['geometry']['coordinates'])
31 | cities.append(coordinates)
32 |
33 | def main():
34 | # Open and create all the necessary files & folders
35 | os.makedirs(args.output, exist_ok=True)
36 |
37 | load_cities()
38 |
39 | coord_output_file = open(os.path.join(args.output, 'picture_coords.csv'), 'w', newline='')
40 | csv_writer = writer(coord_output_file)
41 |
42 | for i in tqdm(range(args.icount)):
43 | cities_count = []
44 | cities_count = [0] * len(cities)
45 | city_index = randint(0, len(cities) - 1)
46 | city = cities[city_index]
47 | cities_count[city_index] += 1
48 | addressLoc = city[randint(0, len(city) - 1)]
49 | city.remove(addressLoc) # Remove the address from the list so we don't get the same one twice
50 | # Set the parameters for the API call to Google Street View
51 | params = {
52 | 'key': args.key,
53 | 'size': '640x640',
54 | 'location': str(addressLoc[1]) + ',' + str(addressLoc[0]),
55 | 'heading': str((randint(0, 3) * 90) + randint(-15, 15)),
56 | 'pitch': '20',
57 | 'fov': '90'
58 | }
59 |
60 | response = requests.get(url, params)
61 |
62 | # Save the image to the output folder
63 | with open(os.path.join(args.output, f'street_view_{i}.jpg'), "wb") as file:
64 | file.write(response.content)
65 |
66 | # Save the coordinates to the output file
67 | csv_writer.writerow([addressLoc[1], addressLoc[0]])
68 |
69 | coord_output_file.close()
70 |
71 | for i in range(len(cities_count)):
72 | city_count = cities_count[i]
73 | city_name = os.listdir(args.cities)[i]
74 | print(f'{city_count} images pulled from {city_name}')
75 |
76 | if __name__ == '__main__':
77 | main()
78 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | from calendar import EPOCH
2 | import os
3 | import argparse
4 | from tqdm import tqdm
5 | import numpy as np
6 | from datetime import datetime
7 |
8 | import torch
9 | import torch.nn as nn
10 | import torch.utils.data
11 | import torchvision.models as models
12 | from torch.utils.tensorboard import SummaryWriter
13 | from utils.tensor_utils import round_tensor
14 | from geoguessr_dataset import GeoGuessrDataset
15 |
16 | model_names = sorted(name for name in models.__dict__
17 | if name.islower() and not name.startswith("__")
18 | and callable(models.__dict__[name]))
19 |
20 | parser = argparse.ArgumentParser(description='PyTorch GeoGuessr AI Training')
21 | parser.add_argument('data', metavar='DIR',
22 | help='path to dataset')
23 | parser.add_argument('-a', '--arch', metavar='ARCH', default='resnet50',
24 | choices=model_names,
25 | help='model architecture: ' +
26 | ' | '.join(model_names) +
27 | ' (default: resnet50)')
28 | parser.add_argument('-j', '--workers', default=4, type=int, metavar='N',
29 | help='number of data loading workers (default: 4)')
30 | parser.add_argument('--epochs', default=90, type=int, metavar='N',
31 | help='number of total epochs to run')
32 | parser.add_argument('--checkpoint-step', default=1, type=int, metavar='N',
33 | help='how often (in epochs) to save the model (default: 1)')
34 | parser.add_argument('-b', '--batch-size', default=64, type=int,
35 | metavar='N',
36 | help='batch size (default: 64), this is the total '
37 | 'batch size of the GPU')
38 | parser.add_argument('--lr', '--learning-rate', default=0.001, type=float,
39 | metavar='LR', help='learning rate for optimizer', dest='lr')
40 | parser.add_argument('-p', '--print-freq', default=10, type=int,
41 | metavar='N', help='print frequency (default: 10)')
42 | parser.add_argument('--resume', default='', type=str, metavar='PATH',
43 | help='path to latest checkpoint (default: none)')
44 | parser.add_argument('-e', '--evaluate', dest='evaluate', action='store_true',
45 | help='evaluate model on validation set')
46 |
47 | start_time = datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
48 | args = parser.parse_args()
49 |
50 | def fwd_pass(model, data, targets, loss_function, optimizer, train=False):
51 | data = data.cuda()
52 | targets = targets.cuda()
53 |
54 | if train:
55 | model.zero_grad()
56 |
57 | outputs = model(data)
58 | matches = [(torch.where(i >= 0.5, 1, 0) == j).all() for i, j in zip(outputs, targets)]
59 | acc = matches.count(True) / len(matches)
60 | loss = loss_function(outputs.float(), targets)
61 |
62 | if train:
63 | loss.backward()
64 | optimizer.step()
65 |
66 | return acc, loss
67 |
68 | def test(val_loader, model, loss_function, optimizer):
69 | random = np.random.randint(len(val_loader))
70 |
71 | model.eval()
72 | acc = []
73 | loss = []
74 |
75 | for idx, sample in enumerate(val_loader):
76 | if idx >= random and idx < random + 4:
77 | data, targets = sample
78 | with torch.no_grad():
79 | val_acc, val_loss = fwd_pass(model, data, targets, loss_function, optimizer)
80 | acc.append(val_acc)
81 | loss.append(val_loss.cpu().numpy())
82 |
83 | val_acc = np.mean(acc)
84 | val_loss = np.mean(loss)
85 | return val_acc, val_loss
86 |
87 | def train(train_loader, val_loader, model, loss_function, optimizer, epochs, start_epoch=0):
88 | with open(f'models/{start_time}/model.log', 'a') as f:
89 | for epoch in range(start_epoch, epochs):
90 | model.train()
91 |
92 | train_acc = []
93 | train_loss = []
94 |
95 | for idx, sample in enumerate(tqdm(train_loader)):
96 | data, target = sample
97 | acc, loss = fwd_pass(model, data, target, loss_function, optimizer, train=True)
98 | train_acc.append(acc)
99 | train_loss.append(loss.cpu().detach().numpy())
100 |
101 | acc = np.mean(train_acc)
102 | loss = np.mean(train_loss)
103 |
104 | val_acc, val_loss = test(val_loader, model, loss_function, optimizer)
105 |
106 | # Add accuracy and loss to tensorboard
107 | progress = len(train_loader) / idx
108 | writer.add_scalar('Loss/train', loss, epoch)
109 | writer.add_scalar('Accuracy/train', acc, epoch)
110 | writer.add_scalar('Loss/test', val_loss, epoch)
111 | writer.add_scalar('Accuracy/test', val_acc, epoch)
112 |
113 | # Log Accuracy and Loss
114 | log = f'model-{epoch}, Accuracy: {round(float(acc), 2)}, Loss: {round(float(loss), 4)}, Val Accuracy: {round(float(val_acc), 2)}, Val Loss: {round(float(val_loss), 4)}\n'
115 | print(log, end='')
116 | f.write(log)
117 |
118 | if epoch % args.checkpoint_step == 0:
119 | print('Saving model...')
120 | torch.save({
121 | 'epoch': epoch,
122 | 'model_state_dict': model.state_dict(),
123 | 'optimizer_state_dict': optimizer.state_dict(),
124 | 'loss': loss
125 | }, f'models/{start_time}/model-{epoch}.pth')
126 |
127 | def main():
128 | global writer
129 | writer = SummaryWriter(f'tensorboard/{start_time}')
130 |
131 | os.makedirs(f'models/{start_time}', exist_ok=True)
132 |
133 | traindir = os.path.join(args.data, 'train')
134 | valdir = os.path.join(args.data, 'val')
135 | train_dataset = GeoGuessrDataset(traindir)
136 | val_dataset = GeoGuessrDataset(valdir)
137 |
138 | train_loader = torch.utils.data.DataLoader(
139 | train_dataset, batch_size=args.batch_size, shuffle=True,
140 | num_workers=args.workers, pin_memory=True)
141 |
142 | val_loader = torch.utils.data.DataLoader(
143 | val_dataset, batch_size=args.batch_size, shuffle=False,
144 | num_workers=args.workers, pin_memory=True)
145 |
146 | print("=> creating model '{}'".format(args.arch))
147 | model = models.__dict__[args.arch](pretrained=False, progress=True, num_classes=142)
148 | model = nn.Sequential(
149 | model,
150 | nn.Softmax(dim=0)
151 | )
152 |
153 | loss_function = nn.CrossEntropyLoss()
154 |
155 | if torch.cuda.is_available():
156 | print('Using GPU')
157 | torch.device("cuda")
158 | model = model.cuda()
159 | loss_function = loss_function.cuda()
160 | else:
161 | print('Using CPU')
162 | torch.device("cpu")
163 |
164 | optimizer = torch.optim.Adam(model.parameters(), args.lr, weight_decay=1e-4)
165 |
166 | start_epoch = 0
167 |
168 | if not args.resume == '':
169 | checkpoint = torch.load(args.resume)
170 | model.load_state_dict(checkpoint['model_state_dict'])
171 | optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
172 | start_epoch = checkpoint['epoch'] + 1
173 | print(f'Resuming from epoch {start_epoch}')
174 |
175 | EPOCHS = args.epochs
176 | train(train_loader=train_loader, val_loader=val_loader, model=model, loss_function=loss_function, optimizer=optimizer, epochs=EPOCHS, start_epoch=start_epoch)
177 |
178 | if __name__ == '__main__':
179 | main()
180 |
--------------------------------------------------------------------------------
/notebook/GeoGuessr_AI_Demo.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "nbformat": 4,
3 | "nbformat_minor": 0,
4 | "metadata": {
5 | "colab": {
6 | "name": "GeoGuessr-AI Demo",
7 | "provenance": [],
8 | "authorship_tag": "ABX9TyP5rZRIlS0xIT6+iHUu0CqK",
9 | "include_colab_link": true
10 | },
11 | "kernelspec": {
12 | "name": "python3",
13 | "display_name": "Python 3"
14 | },
15 | "language_info": {
16 | "name": "python"
17 | }
18 | },
19 | "cells": [
20 | {
21 | "cell_type": "markdown",
22 | "metadata": {
23 | "id": "view-in-github",
24 | "colab_type": "text"
25 | },
26 | "source": [
27 | "
"
28 | ]
29 | },
30 | {
31 | "cell_type": "markdown",
32 | "source": [
33 | "# GeoGuessr-AI Demo"
34 | ],
35 | "metadata": {
36 | "id": "XYROTdQlbE6x"
37 | }
38 | },
39 | {
40 | "cell_type": "markdown",
41 | "source": [
42 | "Disclaimer, this model was only trained on pictures from 5 cities (Washington, City of New York, Chicago, Detroit, and San Francisco) and will therefore only be able to predict the location of photos taken in those cities."
43 | ],
44 | "metadata": {
45 | "id": "mPqEzY1MIfwQ"
46 | }
47 | },
48 | {
49 | "cell_type": "markdown",
50 | "source": [
51 | "## Setup"
52 | ],
53 | "metadata": {
54 | "id": "hToy8jcKbOjx"
55 | }
56 | },
57 | {
58 | "cell_type": "code",
59 | "execution_count": 1,
60 | "metadata": {
61 | "colab": {
62 | "base_uri": "https://localhost:8080/"
63 | },
64 | "id": "d17FwqlWXsTd",
65 | "outputId": "9dec4987-f9a7-438e-e3cf-0c1425203300"
66 | },
67 | "outputs": [
68 | {
69 | "output_type": "stream",
70 | "name": "stdout",
71 | "text": [
72 | "Downloading...\n",
73 | "From: https://drive.google.com/uc?id=1VJpeLJp6jC8IUfKy6cAtZ9WZcX1TTutW&confirm=t\n",
74 | "To: /content/geoguessr_production_model.pt\n",
75 | "100% 269M/269M [00:01<00:00, 176MB/s]\n"
76 | ]
77 | }
78 | ],
79 | "source": [
80 | "!gdown 'https://drive.google.com/uc?id=1VJpeLJp6jC8IUfKy6cAtZ9WZcX1TTutW&confirm=t'"
81 | ]
82 | },
83 | {
84 | "cell_type": "code",
85 | "source": [
86 | "import torch\n",
87 | "import torchvision.models as models\n",
88 | "import torchvision.transforms as transforms\n",
89 | "import torch.nn as nn\n",
90 | "\n",
91 | "import numpy as np\n",
92 | "from PIL import Image\n",
93 | "import os\n",
94 | "\n",
95 | "from google.colab import files\n",
96 | "\n",
97 | "model = models.wide_resnet50_2(pretrained=False, progress=True, num_classes=142)\n",
98 | "model = nn.Sequential(\n",
99 | " model,\n",
100 | " nn.Sigmoid()\n",
101 | ")\n",
102 | "model_file = torch.load('geoguessr_production_model.pt', map_location=torch.device('cpu'))\n",
103 | "model.load_state_dict(model_file)\n",
104 | "model.eval()\n",
105 | "print('Loaded Model')\n",
106 | "\n",
107 | "def reformat(arr, guess_num=1):\n",
108 | " num = ''\n",
109 | " if arr[0] >= 0.5:\n",
110 | " num += '-'\n",
111 | " \n",
112 | " arr = arr[1:]\n",
113 | "\n",
114 | " for idx in range(0, len(arr), 10):\n",
115 | " if idx == 30:\n",
116 | " num += '.'\n",
117 | " num += str(np.where(arr[idx:idx+10] == np.partition(arr[idx:idx+10].flatten(), -guess_num)[-guess_num])[0][0] % 10)\n",
118 | "\n",
119 | " return num\n",
120 | "\n",
121 | "normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406],\n",
122 | " std=[0.229, 0.224, 0.225])\n",
123 | "transform = transforms.Compose([\n",
124 | " transforms.Resize((256, 256)),\n",
125 | " transforms.ToTensor(),\n",
126 | " normalize,\n",
127 | " ])"
128 | ],
129 | "metadata": {
130 | "colab": {
131 | "base_uri": "https://localhost:8080/"
132 | },
133 | "id": "AiJcPzD0bmT2",
134 | "outputId": "1787a5a5-67c4-4381-dbfe-09f9e7aa0c28"
135 | },
136 | "execution_count": 2,
137 | "outputs": [
138 | {
139 | "output_type": "stream",
140 | "name": "stdout",
141 | "text": [
142 | "Loaded Model\n"
143 | ]
144 | }
145 | ]
146 | },
147 | {
148 | "cell_type": "markdown",
149 | "source": [
150 | "## Run Model"
151 | ],
152 | "metadata": {
153 | "id": "vbWYVDdocvN0"
154 | }
155 | },
156 | {
157 | "cell_type": "code",
158 | "source": [
159 | "#@title ## Load File\n",
160 | "\n",
161 | "reference_file = files.upload()\n",
162 | "reference_file = list(reference_file.keys())[0]\n",
163 | "\n",
164 | "img_path = os.path.join(reference_file)\n",
165 | "img = Image.open(img_path)\n",
166 | "data = transform(img)\n",
167 | "\n",
168 | "from matplotlib import pyplot as plt\n",
169 | "img = data.permute(1, 2, 0)\n",
170 | "plt.imshow(img, interpolation='nearest')\n",
171 | "plt.show()"
172 | ],
173 | "metadata": {
174 | "colab": {
175 | "resources": {
176 | "http://localhost:8080/nbextensions/google.colab/files.js": {
177 | "data": "Ly8gQ29weXJpZ2h0IDIwMTcgR29vZ2xlIExMQwovLwovLyBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKLy8geW91IG1heSBub3QgdXNlIHRoaXMgZmlsZSBleGNlcHQgaW4gY29tcGxpYW5jZSB3aXRoIHRoZSBMaWNlbnNlLgovLyBZb3UgbWF5IG9idGFpbiBhIGNvcHkgb2YgdGhlIExpY2Vuc2UgYXQKLy8KLy8gICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKLy8KLy8gVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQovLyBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAovLyBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IgaW1wbGllZC4KLy8gU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAovLyBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi8qKgogKiBAZmlsZW92ZXJ2aWV3IEhlbHBlcnMgZm9yIGdvb2dsZS5jb2xhYiBQeXRob24gbW9kdWxlLgogKi8KKGZ1bmN0aW9uKHNjb3BlKSB7CmZ1bmN0aW9uIHNwYW4odGV4dCwgc3R5bGVBdHRyaWJ1dGVzID0ge30pIHsKICBjb25zdCBlbGVtZW50ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnc3BhbicpOwogIGVsZW1lbnQudGV4dENvbnRlbnQgPSB0ZXh0OwogIGZvciAoY29uc3Qga2V5IG9mIE9iamVjdC5rZXlzKHN0eWxlQXR0cmlidXRlcykpIHsKICAgIGVsZW1lbnQuc3R5bGVba2V5XSA9IHN0eWxlQXR0cmlidXRlc1trZXldOwogIH0KICByZXR1cm4gZWxlbWVudDsKfQoKLy8gTWF4IG51bWJlciBvZiBieXRlcyB3aGljaCB3aWxsIGJlIHVwbG9hZGVkIGF0IGEgdGltZS4KY29uc3QgTUFYX1BBWUxPQURfU0laRSA9IDEwMCAqIDEwMjQ7CgpmdW5jdGlvbiBfdXBsb2FkRmlsZXMoaW5wdXRJZCwgb3V0cHV0SWQpIHsKICBjb25zdCBzdGVwcyA9IHVwbG9hZEZpbGVzU3RlcChpbnB1dElkLCBvdXRwdXRJZCk7CiAgY29uc3Qgb3V0cHV0RWxlbWVudCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKG91dHB1dElkKTsKICAvLyBDYWNoZSBzdGVwcyBvbiB0aGUgb3V0cHV0RWxlbWVudCB0byBtYWtlIGl0IGF2YWlsYWJsZSBmb3IgdGhlIG5leHQgY2FsbAogIC8vIHRvIHVwbG9hZEZpbGVzQ29udGludWUgZnJvbSBQeXRob24uCiAgb3V0cHV0RWxlbWVudC5zdGVwcyA9IHN0ZXBzOwoKICByZXR1cm4gX3VwbG9hZEZpbGVzQ29udGludWUob3V0cHV0SWQpOwp9CgovLyBUaGlzIGlzIHJvdWdobHkgYW4gYXN5bmMgZ2VuZXJhdG9yIChub3Qgc3VwcG9ydGVkIGluIHRoZSBicm93c2VyIHlldCksCi8vIHdoZXJlIHRoZXJlIGFyZSBtdWx0aXBsZSBhc3luY2hyb25vdXMgc3RlcHMgYW5kIHRoZSBQeXRob24gc2lkZSBpcyBnb2luZwovLyB0byBwb2xsIGZvciBjb21wbGV0aW9uIG9mIGVhY2ggc3RlcC4KLy8gVGhpcyB1c2VzIGEgUHJvbWlzZSB0byBibG9jayB0aGUgcHl0aG9uIHNpZGUgb24gY29tcGxldGlvbiBvZiBlYWNoIHN0ZXAsCi8vIHRoZW4gcGFzc2VzIHRoZSByZXN1bHQgb2YgdGhlIHByZXZpb3VzIHN0ZXAgYXMgdGhlIGlucHV0IHRvIHRoZSBuZXh0IHN0ZXAuCmZ1bmN0aW9uIF91cGxvYWRGaWxlc0NvbnRpbnVlKG91dHB1dElkKSB7CiAgY29uc3Qgb3V0cHV0RWxlbWVudCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKG91dHB1dElkKTsKICBjb25zdCBzdGVwcyA9IG91dHB1dEVsZW1lbnQuc3RlcHM7CgogIGNvbnN0IG5leHQgPSBzdGVwcy5uZXh0KG91dHB1dEVsZW1lbnQubGFzdFByb21pc2VWYWx1ZSk7CiAgcmV0dXJuIFByb21pc2UucmVzb2x2ZShuZXh0LnZhbHVlLnByb21pc2UpLnRoZW4oKHZhbHVlKSA9PiB7CiAgICAvLyBDYWNoZSB0aGUgbGFzdCBwcm9taXNlIHZhbHVlIHRvIG1ha2UgaXQgYXZhaWxhYmxlIHRvIHRoZSBuZXh0CiAgICAvLyBzdGVwIG9mIHRoZSBnZW5lcmF0b3IuCiAgICBvdXRwdXRFbGVtZW50Lmxhc3RQcm9taXNlVmFsdWUgPSB2YWx1ZTsKICAgIHJldHVybiBuZXh0LnZhbHVlLnJlc3BvbnNlOwogIH0pOwp9CgovKioKICogR2VuZXJhdG9yIGZ1bmN0aW9uIHdoaWNoIGlzIGNhbGxlZCBiZXR3ZWVuIGVhY2ggYXN5bmMgc3RlcCBvZiB0aGUgdXBsb2FkCiAqIHByb2Nlc3MuCiAqIEBwYXJhbSB7c3RyaW5nfSBpbnB1dElkIEVsZW1lbnQgSUQgb2YgdGhlIGlucHV0IGZpbGUgcGlja2VyIGVsZW1lbnQuCiAqIEBwYXJhbSB7c3RyaW5nfSBvdXRwdXRJZCBFbGVtZW50IElEIG9mIHRoZSBvdXRwdXQgZGlzcGxheS4KICogQHJldHVybiB7IUl0ZXJhYmxlPCFPYmplY3Q+fSBJdGVyYWJsZSBvZiBuZXh0IHN0ZXBzLgogKi8KZnVuY3Rpb24qIHVwbG9hZEZpbGVzU3RlcChpbnB1dElkLCBvdXRwdXRJZCkgewogIGNvbnN0IGlucHV0RWxlbWVudCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGlucHV0SWQpOwogIGlucHV0RWxlbWVudC5kaXNhYmxlZCA9IGZhbHNlOwoKICBjb25zdCBvdXRwdXRFbGVtZW50ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQob3V0cHV0SWQpOwogIG91dHB1dEVsZW1lbnQuaW5uZXJIVE1MID0gJyc7CgogIGNvbnN0IHBpY2tlZFByb21pc2UgPSBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4gewogICAgaW5wdXRFbGVtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ2NoYW5nZScsIChlKSA9PiB7CiAgICAgIHJlc29sdmUoZS50YXJnZXQuZmlsZXMpOwogICAgfSk7CiAgfSk7CgogIGNvbnN0IGNhbmNlbCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2J1dHRvbicpOwogIGlucHV0RWxlbWVudC5wYXJlbnRFbGVtZW50LmFwcGVuZENoaWxkKGNhbmNlbCk7CiAgY2FuY2VsLnRleHRDb250ZW50ID0gJ0NhbmNlbCB1cGxvYWQnOwogIGNvbnN0IGNhbmNlbFByb21pc2UgPSBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4gewogICAgY2FuY2VsLm9uY2xpY2sgPSAoKSA9PiB7CiAgICAgIHJlc29sdmUobnVsbCk7CiAgICB9OwogIH0pOwoKICAvLyBXYWl0IGZvciB0aGUgdXNlciB0byBwaWNrIHRoZSBmaWxlcy4KICBjb25zdCBmaWxlcyA9IHlpZWxkIHsKICAgIHByb21pc2U6IFByb21pc2UucmFjZShbcGlja2VkUHJvbWlzZSwgY2FuY2VsUHJvbWlzZV0pLAogICAgcmVzcG9uc2U6IHsKICAgICAgYWN0aW9uOiAnc3RhcnRpbmcnLAogICAgfQogIH07CgogIGNhbmNlbC5yZW1vdmUoKTsKCiAgLy8gRGlzYWJsZSB0aGUgaW5wdXQgZWxlbWVudCBzaW5jZSBmdXJ0aGVyIHBpY2tzIGFyZSBub3QgYWxsb3dlZC4KICBpbnB1dEVsZW1lbnQuZGlzYWJsZWQgPSB0cnVlOwoKICBpZiAoIWZpbGVzKSB7CiAgICByZXR1cm4gewogICAgICByZXNwb25zZTogewogICAgICAgIGFjdGlvbjogJ2NvbXBsZXRlJywKICAgICAgfQogICAgfTsKICB9CgogIGZvciAoY29uc3QgZmlsZSBvZiBmaWxlcykgewogICAgY29uc3QgbGkgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdsaScpOwogICAgbGkuYXBwZW5kKHNwYW4oZmlsZS5uYW1lLCB7Zm9udFdlaWdodDogJ2JvbGQnfSkpOwogICAgbGkuYXBwZW5kKHNwYW4oCiAgICAgICAgYCgke2ZpbGUudHlwZSB8fCAnbi9hJ30pIC0gJHtmaWxlLnNpemV9IGJ5dGVzLCBgICsKICAgICAgICBgbGFzdCBtb2RpZmllZDogJHsKICAgICAgICAgICAgZmlsZS5sYXN0TW9kaWZpZWREYXRlID8gZmlsZS5sYXN0TW9kaWZpZWREYXRlLnRvTG9jYWxlRGF0ZVN0cmluZygpIDoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ24vYSd9IC0gYCkpOwogICAgY29uc3QgcGVyY2VudCA9IHNwYW4oJzAlIGRvbmUnKTsKICAgIGxpLmFwcGVuZENoaWxkKHBlcmNlbnQpOwoKICAgIG91dHB1dEVsZW1lbnQuYXBwZW5kQ2hpbGQobGkpOwoKICAgIGNvbnN0IGZpbGVEYXRhUHJvbWlzZSA9IG5ldyBQcm9taXNlKChyZXNvbHZlKSA9PiB7CiAgICAgIGNvbnN0IHJlYWRlciA9IG5ldyBGaWxlUmVhZGVyKCk7CiAgICAgIHJlYWRlci5vbmxvYWQgPSAoZSkgPT4gewogICAgICAgIHJlc29sdmUoZS50YXJnZXQucmVzdWx0KTsKICAgICAgfTsKICAgICAgcmVhZGVyLnJlYWRBc0FycmF5QnVmZmVyKGZpbGUpOwogICAgfSk7CiAgICAvLyBXYWl0IGZvciB0aGUgZGF0YSB0byBiZSByZWFkeS4KICAgIGxldCBmaWxlRGF0YSA9IHlpZWxkIHsKICAgICAgcHJvbWlzZTogZmlsZURhdGFQcm9taXNlLAogICAgICByZXNwb25zZTogewogICAgICAgIGFjdGlvbjogJ2NvbnRpbnVlJywKICAgICAgfQogICAgfTsKCiAgICAvLyBVc2UgYSBjaHVua2VkIHNlbmRpbmcgdG8gYXZvaWQgbWVzc2FnZSBzaXplIGxpbWl0cy4gU2VlIGIvNjIxMTU2NjAuCiAgICBsZXQgcG9zaXRpb24gPSAwOwogICAgZG8gewogICAgICBjb25zdCBsZW5ndGggPSBNYXRoLm1pbihmaWxlRGF0YS5ieXRlTGVuZ3RoIC0gcG9zaXRpb24sIE1BWF9QQVlMT0FEX1NJWkUpOwogICAgICBjb25zdCBjaHVuayA9IG5ldyBVaW50OEFycmF5KGZpbGVEYXRhLCBwb3NpdGlvbiwgbGVuZ3RoKTsKICAgICAgcG9zaXRpb24gKz0gbGVuZ3RoOwoKICAgICAgY29uc3QgYmFzZTY0ID0gYnRvYShTdHJpbmcuZnJvbUNoYXJDb2RlLmFwcGx5KG51bGwsIGNodW5rKSk7CiAgICAgIHlpZWxkIHsKICAgICAgICByZXNwb25zZTogewogICAgICAgICAgYWN0aW9uOiAnYXBwZW5kJywKICAgICAgICAgIGZpbGU6IGZpbGUubmFtZSwKICAgICAgICAgIGRhdGE6IGJhc2U2NCwKICAgICAgICB9LAogICAgICB9OwoKICAgICAgbGV0IHBlcmNlbnREb25lID0gZmlsZURhdGEuYnl0ZUxlbmd0aCA9PT0gMCA/CiAgICAgICAgICAxMDAgOgogICAgICAgICAgTWF0aC5yb3VuZCgocG9zaXRpb24gLyBmaWxlRGF0YS5ieXRlTGVuZ3RoKSAqIDEwMCk7CiAgICAgIHBlcmNlbnQudGV4dENvbnRlbnQgPSBgJHtwZXJjZW50RG9uZX0lIGRvbmVgOwoKICAgIH0gd2hpbGUgKHBvc2l0aW9uIDwgZmlsZURhdGEuYnl0ZUxlbmd0aCk7CiAgfQoKICAvLyBBbGwgZG9uZS4KICB5aWVsZCB7CiAgICByZXNwb25zZTogewogICAgICBhY3Rpb246ICdjb21wbGV0ZScsCiAgICB9CiAgfTsKfQoKc2NvcGUuZ29vZ2xlID0gc2NvcGUuZ29vZ2xlIHx8IHt9OwpzY29wZS5nb29nbGUuY29sYWIgPSBzY29wZS5nb29nbGUuY29sYWIgfHwge307CnNjb3BlLmdvb2dsZS5jb2xhYi5fZmlsZXMgPSB7CiAgX3VwbG9hZEZpbGVzLAogIF91cGxvYWRGaWxlc0NvbnRpbnVlLAp9Owp9KShzZWxmKTsK",
178 | "ok": true,
179 | "headers": [
180 | [
181 | "content-type",
182 | "application/javascript"
183 | ]
184 | ],
185 | "status": 200,
186 | "status_text": ""
187 | }
188 | },
189 | "base_uri": "https://localhost:8080/",
190 | "height": 342
191 | },
192 | "cellView": "form",
193 | "id": "hKdi894nbyKp",
194 | "outputId": "662520b0-9ae2-4406-873a-49adfb3fe830"
195 | },
196 | "execution_count": null,
197 | "outputs": [
198 | {
199 | "output_type": "display_data",
200 | "data": {
201 | "text/html": [
202 | "\n",
203 | " \n",
205 | " \n",
209 | " "
210 | ],
211 | "text/plain": [
212 | ""
213 | ]
214 | },
215 | "metadata": {}
216 | },
217 | {
218 | "output_type": "stream",
219 | "name": "stdout",
220 | "text": [
221 | "Saving sample_image.jpeg to sample_image.jpeg\n"
222 | ]
223 | },
224 | {
225 | "output_type": "stream",
226 | "name": "stderr",
227 | "text": [
228 | "Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).\n"
229 | ]
230 | },
231 | {
232 | "output_type": "display_data",
233 | "data": {
234 | "image/png": "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\n",
235 | "text/plain": [
236 | ""
237 | ]
238 | },
239 | "metadata": {
240 | "needs_background": "light"
241 | }
242 | }
243 | ]
244 | },
245 | {
246 | "cell_type": "code",
247 | "source": [
248 | "#@title ## Make Prediction\n",
249 | "#@markdown Run the model and get a lattitude and longitude prediction based on the image, the model will predict the most likely location and then make a backup guess as well.\n",
250 | "\n",
251 | "with torch.no_grad():\n",
252 | " data = data.view(-1,3,256,256)\n",
253 | " output = model(data)\n",
254 | "\n",
255 | "target_split_len = int(len(output.cpu().numpy()[0])/2)\n",
256 | "output_reformatted = reformat(output.cpu().numpy()[0][:target_split_len]) + ' ' + reformat(output.cpu().numpy()[0][target_split_len:])\n",
257 | "output_reformatted2 = reformat(output.cpu().numpy()[0][:target_split_len]) + ' ' + reformat(output.cpu().numpy()[0][target_split_len:], 2)\n",
258 | "print('First Guess:', output_reformatted)\n",
259 | "print('Second Guess:', output_reformatted2)"
260 | ],
261 | "metadata": {
262 | "colab": {
263 | "base_uri": "https://localhost:8080/"
264 | },
265 | "id": "XWavPNf1c6Fy",
266 | "outputId": "be208b02-a41e-4399-fbf0-6bc9eb377624",
267 | "cellView": "form"
268 | },
269 | "execution_count": null,
270 | "outputs": [
271 | {
272 | "output_type": "stream",
273 | "name": "stdout",
274 | "text": [
275 | "First Guess: 037.7838 -122.4105\n",
276 | "Second Guess: 037.7838 -073.3263\n"
277 | ]
278 | }
279 | ]
280 | }
281 | ]
282 | }
--------------------------------------------------------------------------------
/save_production_model.py:
--------------------------------------------------------------------------------
1 | import argparse
2 |
3 | import torch
4 | import torch.utils.data
5 |
6 | parser = argparse.ArgumentParser(description='PyTorch GeoGuessr AI Best Model Locator')
7 | parser.add_argument('modelpath', metavar='DIR',
8 | help='path to model')
9 |
10 | args = parser.parse_args()
11 |
12 | def main():
13 | checkpoint = torch.load(args.modelpath)
14 | torch.save(checkpoint['model_state_dict'], 'geoguessr_production_model.pt')
15 |
16 | if __name__ == '__main__':
17 | main()
--------------------------------------------------------------------------------
/utils/tensor_utils.py:
--------------------------------------------------------------------------------
1 | import torch
2 |
3 | def round_tensor(tensor, decimals=4):
4 | return torch.round(tensor * 10 ** decimals) / (10 ** decimals)
--------------------------------------------------------------------------------