├── .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 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Stelath/geoguessr-ai/blob/main/notebook/GeoGuessr_AI_Demo.ipynb) ![License](https://img.shields.io/github/license/Stelath/geoguessr-ai) 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: [![Open In Drive](https://img.shields.io/badge/Google%20Drive-5383ec?style=flat&logo=googledrive&logoColor=5383ec&label=%E2%80%8B)](https://drive.google.com/file/d/1VJpeLJp6jC8IUfKy6cAtZ9WZcX1TTutW/view?usp=sharing) or you can use the Google Colab: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](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 | "\"Open" 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", 206 | " Upload widget is only available when the cell has been executed in the\n", 207 | " current browser session. Please rerun this cell to enable.\n", 208 | " \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) --------------------------------------------------------------------------------